From d2dd7968d8edefac1e52385d93c6274507ad1525 Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Tue, 10 May 2022 13:31:31 -0700 Subject: [PATCH 1/2] BHBC-1700: Move to Prod (#775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * First commit (#566) * First commit * Upload * Update the sec produre for unseceruing an attachment * Version update * Update for single upload button * New fixtures * Update * Survey script * delete survey * Spacing * Moving to test (#611) * Update README.md * First commit (#566) * First commit * Upload * Update the sec produre for unseceruing an attachment * Version update * Update for single upload button * New fixtures * Update * Survey script * delete survey * Spacing * Update access request page: remove regional offices section, remove non-supported roles from dropdown (#610) * [BHBC-1449] Add Help Link (#614) * Update template validation schemas (#612) * Add migration to update moose SRB/Composition Validation schema * Add Summary Statistics file to resources page * Add Date column to recruitment survey * Fix dependency warnings * Remove unused test function * Update deployStatic.yml * Move into test (#615) * Update README.md * First commit (#566) * First commit * Upload * Update the sec produre for unseceruing an attachment * Version update * Update for single upload button * New fixtures * Update * Survey script * delete survey * Spacing * Update access request page: remove regional offices section, remove non-supported roles from dropdown (#610) * [BHBC-1449] Add Help Link (#614) * Update template validation schemas (#612) * Add migration to update moose SRB/Composition Validation schema * Add Summary Statistics file to resources page * Add Date column to recruitment survey * Fix dependency warnings * Remove unused test function * Update deployStatic.yml Co-authored-by: Nick Phura Co-authored-by: jeznorth * BHBC-1363 (#599) Support project/survey report attachments with meta * Add admin only endpoint to update the api log level at runtime (#619) * Add admin only endpoint to update the api log level at runtime * Update test * Adding Test scripts (#617) * Adding Test scripts * Create project and Survey * Remove second test file * Test Updates/Fixes (#620) Test updates/Fixes * BHBC-1363-2: Fix signed url for reports, update fileupload to support single file only settings. (#621) * BHBC-1461 (#622) * Readme makefile updates (#623) * Update Makefile, Readme, env * Rename BioHub -> SIMS * Add `Order By` clause to species sql (#627) * BHBC-1462: Add new record to proprietary_type lookup table. (#629) - Update code to leverage the `is_first_nation` column from the proprietary_type table. - Previously the code was using the row id * BHBC-1470: View/edit survey and project metadata (#624) BHBC-1470: View/edit survey and project metadata (#624) * BHBC-1478: Update API to check project level permissions. (#630) * Combine dependabot (#634) * Bump tmpl from 1.0.4 to 1.0.5 in /app Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/daaku/nodejs-tmpl/releases) - [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5) --- updated-dependencies: - dependency-name: tmpl dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump ws from 5.2.2 to 5.2.3 in /app Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump validator from 13.6.0 to 13.7.0 in /n8n Bumps [validator](https://github.com/validatorjs/validator.js) from 13.6.0 to 13.7.0. - [Release notes](https://github.com/validatorjs/validator.js/releases) - [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/validatorjs/validator.js/compare/13.6.0...13.7.0) --- updated-dependencies: - dependency-name: validator dependency-type: indirect ... Signed-off-by: dependabot[bot] * Update Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * migration (#637) * Bhbc 1470 (#638) * Small test script and seeding changes (#631) * Small test script and seeding changes * Remove Shreyas from the seed Co-authored-by: Nick Phura * Create zap.yml * Update zap.yml * Create dev.context * Update zap.yml * Zap follow up (#640) * Small test script and seeding changes * Remove Shreyas from the seed * Details for ZAP * Subtitute hardcoded PW with GHA- Secret * [BHBC-1470] Increase Report Summary Input Size (#641) * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * BHBC-1467: One page project/survey layout (#636) One page project/survey layout * Test Script update, login and some other details (#646) * Test Script update, login and some other details * Add delay to login * Remove file * Pointing to the right authurl * Path confusion * And again * It is all in the timing * Update readme as per Nick's comment * BHBC-950 (#643) - System admin users can delete other system users. * Update DialogContext.Snackbar(#647) Update snackbar to accept ReactNode as message * Update deployStatic.yml * [BHBC-950] UI Cleanup (#648) * [BHBC-950] UI Cleanup * [BHBC-950] Updating UI * Trigger Build * Update to test script to deal with changed id for report upload * [BHBC-950] Fixing Request Access Table Co-authored-by: Roland Stens Co-authored-by: Anissa Agahchen * Update deployStatic.yml * BHBC-949: Manage Project Team - Add + Delete User (#645) - Add project users page - Add project users "add project participant" form - Add delete project user functionality - Update some api project role authorization checks - Update some frontend role check functionality - misc fixes/updates * Update deployStatic.yml * First test (#650) * First test * Update path * - update older node versions to 14 - Increase tsconfig versions - Increase a few package.json versions - Update a few git actions to latest versions * Revert accidental backup file change * Set back to es2015 otherwise the test framework will fail Co-authored-by: Nick Phura * Update deployStatic.yml * Manage system/project roles (#649) * BHBC-948: Allow users to change existing system user roles. * BHBC-1491: Allow users to change existing project participant's roles. * Update deployStatic.yml * [BHBC-948] UI Updates / Cleanup (#652) * Updates to Agancies and Proeject Coordinator name/label * Revert "Merge branch 'dev' of https://github.com/bcgov/biohubbc into dev" This reverts commit 16b5f48a44f0b791b25e7f2b36c0ede5d0db182f, reversing changes made to 1c88d34ead5ea84bdb03825e3a0667eb7de3e57b. * Changes to codes.ts for removing BC and all over for the coordinator vs contact name … (#654) * Cahnges to codes.ts and all over for the coordinator ro contact name change * Update snapshots * Finish BHBC-1505 * Update deployStatic.yml * BHBC-1457 (#651) Implemented EML endpoint and addition to target DwC archive file on S3. Moved all EML creation functionality to middle tier tooling. * Tech debt (#653) * Split up the workflow for uploading project/survey attachments and project/survey reports * associated tests * Updates to the test script and table changes (#658) * All changes * Update to formatting * Remove new table * Bhbc 1499 (#656) Updates to the Moose template and transformation configs * BHBC-1527: system and project roles updated (#659) * update url for download (#660) * Fix wonky survey view/view-for-update queries + misc (#664) * Fix survey view/view-for-update queries + misc * BHBC-1496: Transform Lat/Long to Darwin Core Accepted Format (#667) - Enhance transformations to allow multiple source columns in parse step (uses first non-null value) - Update scrape SQL to account for UTM or LatLong verbatimCoordinates - Add `parseLatLongString` spatial-util function to parse a lat long string. * BHBC-1525 (#669) * BHBC-1525 - initial EML test file and low hanging target testing * remove unused system accounts (#666) * remove unused system accounts * update code to reference previously updated role names * Security rules for PorH (#668) * Security rules for PorH * Endpoint Tests (#661) * Add missing `connection.commit`'s (#671) * Update endpoint role authorization (#670) * Update endpoint role authorization * Update occurrence scrape verbatimCoordinates handling * Bhbc 1400 (#657) Goat template validations and transformations Sheep template validations and transformations * UI Updates / Clean-up (#674) * Update all the BC and DC to tune the resource usage (#678) * BHBC-1523: Add new users (#675) * Added "+ new users" button on Admin user page and added testing for both API and app. * Bhbc 1529- Add custom props to templates (#677) * BHBC-1530: Update Moose transformations to account for all fields. (#680) * BHBC-1530 - Enhance xlsx transformation parse step - Update Moose validation/transformation schemas - Minor refactor to part of transformation code - Update resource page link * BHBC-1520: View user details (#679) - Ability to add system users from the manage users page - Associated endpoints and tests - Page to view user details and their associated projects - Ability to remove user from projects or update their role. - Associated endpoints and tests - Reduce some api code duplication * BHBC-1531 (#683) * BHBC-1531 - adjusted taxonomic codes to exclude those codes that have end date setc * Fix focal species yup schema validation rule * BHBC-1520 (#684) Prevent projects from having no Project Lead role: bug fixes + tests * BHBC-1512: Added zoom for fullscreen map on project details. (#685) Added zoom for fullscreen map on project details * Enhance useInterval hook to support a timeout. Add unit tests. (#686) * BHBC-1512-2: Additional map scroll wheel zoom changes (#687) * BHBC-1512-2: Additional map scroll wheel zoom changes * BHBC-1545: Tech Debt: Re-export all queries via queries.ts. Update custom error naming. (#688) Add index files for all query folders Add queries.ts to re-export all queries. Update all references to existing queries to instead reference queries.ts Rename `CustomError.ts->CustomError` to `custom-error.ts->HTTPError` and all references. Run formatter/linter. * BHBC-1545: Tech Debt: Updated apidoc for /user/* (#681) Update openapi doc for /user/* endpoints Misc unit test updates * BHBC-1503: GCNotfiy (#689) * Created GCNotify Api endpoint for generic email functionality * BHBC-1557: Tech Debt 3 (#690) * Add user-service.ts Move user functions to user-service.ts. Update tests. Add code-service.ts Move code functions to code-service.ts Update tests. Misc root-api-doc.ts cleanup Add swagger-ui container to docker-compose * Add SwaggerUI to docker-compose.yml (for local dev currently) * Minor makefile updates * Updates to error classes * Updates to error logging * Minor test tweak * Handle DatabaseErrors * remove console logs * BHBC-1492: Email Access Request (#692) * BHBC-1557: Tech Debt - Update App route handling and route security. (#693) * Tech debt: Fix partnerships, and update /project/{projectId}/update endpoint (#694) * BHBC-1557: Ke-tech-debt (#698) * updated openApi schemas * Added Survey creation. (#700) * Unify dependabot (#701) * Bump nanoid from 3.1.30 to 3.2.0 in /n8n (#704) * Bump copy-props from 2.0.4 to 2.0.5 in /api (#702) * Bump node-fetch from 2.6.5 to 2.6.7 in /n8n (#705) * BHBC-1557: Add ProjectService. (#699) * Bump lodash from 4.17.20 to 4.17.21 in /app (#703) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) --- updated-dependencies: - dependency-name: lodash dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anissa Agahchen * BHBC-1493: Approval Email For Access Request (#708) Approval email sent to idir email connected to access request user. * Dev ops scale schemaspy up and down (#709) * Scale SchemaSpy as part of the pipeline to make sure it is current all the time * Make it a one step SchemaSpy cycle * Formatting * Fix typo * Reduce cpu/request limits * Revert "Reduce cpu/request limits" This reverts commit b99f8cbcfcd027e75c16370eb8afd9913102a15c. * Reduce cpu/request limits + Adjust default Log Level (#716) * BCEID bug fix + improvements (#717) * BHBC-1613 - support bceid login (#720) * BHBC-1580: Updates to moose/sheep/goat templates (#725) * Tech debt 6 (#707) * Disable html logging when running app tests. * Update logger utils, make mocha disable winston logger before test execution * Add missing type definition for @tmcw/togeojson * Updates to map components (from resto) * Update typescript and knex versions, remove swagger ui docker compose step, add swagger-ui directly to app * Incorporate dependabot changes * Update eslint config * Run lint-fix using updated eslint rules and fix issues * Update startend date component * Move old `/users` endpoint to `/user/list` * First cut of the postman collection with automatic login (#726) * Isolating Service Code (#724) Port changes from restoration to move functionality to services * Update siteminder url (#736) * Survey- Additional Fields (#731) * create/view/edit survey page with new purpose and methodology form * addition of "ecological season", "intended outcome" and "vantage" lookup tables & removal of common survey methodology - field table name change to "field method" * "survey.objectives" attribute name change to "survey.additional_details" * seeding new lookup tables * re-seed "field method" table * tests * package lock updates for minimist (#740) * dependabot change version for moment (#739) * Bump convict from 6.2.0 to 6.2.2 in /n8n (#745) Bumps [convict](https://github.com/mozilla/node-convict) from 6.2.0 to 6.2.2. - [Release notes](https://github.com/mozilla/node-convict/releases) - [Changelog](https://github.com/mozilla/node-convict/blob/master/CHANGELOG.md) - [Commits](https://github.com/mozilla/node-convict/compare/v6.2.0...v6.2.2) --- updated-dependencies: - dependency-name: convict dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump async from 2.6.3 to 2.6.4 in /app (#743) Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump async from 3.2.0 to 3.2.3 in /testing/e2e (#742) Bumps [async](https://github.com/caolan/async) from 3.2.0 to 3.2.3. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/master/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v3.2.0...v3.2.3) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * BHBC-1688: Add survey radio field (#746) * BHBC-1688: Add new radio button field to survey. * Add github action to write openshift urls * Techdebt (#741) remove default logs fix tests fix code smells * Misc Cleanup (#748) * misc cleanup * Update user seed file * BHBC-1690: Add scroll to error formik component (#749) * Scroll to survey form errors. * Fix vantage codes bug that returned an array with a null element (#750) * Sims taxonomy (#744) * app is working with the taxonomy service * updated snapshots * tests * survey service set up, using it for survey list * skipping broken tests * Remove `wldtaxonomic_units` table from survey queries * merge conflicts fixed * skipped some tests * add TODO comment * Trigger Build * Fix some tests * fixes attachmentList * fixed tests * Update project page test * surveysList test updated * Update ProjectsListPage.test.tsx * Update ReviewAccessRequestForm.test.tsx * test for Active Users List * fix more tests * code smells * Remove unused imports * fix general information bug * fix warning on general information form Co-authored-by: Nick Phura Co-authored-by: Kjartan * BHBC-1689: Implement API Response Validation (#737) * Add api response validation * Updates * update reponse for the project attachments list * BHBC-1689: Add validation to projects * BHBC-1689: Add response validation to surveys + attachments + observations * BHBC-1689: Add response validation to project attachments. * BHBC-1689: Fix publish_year type for report attachment uploading via POST. * BHBC-1689: Fix api response validation for user data response. * BHBC-1689: Change year_published type from string to number in attachment tests. * BHBC-1689: Update tests * Update occurrence submission response openapi * BHBC-1689: Fix api response validation for uploads * Update summary submission openapi and parse logic * BHBC-1689: Fix all failing tests for reports * BHBC-1689: Make lint-fix and make format-fix. * BHBC-1689: Remove console logs used for testing. * api validation for view.ts and surveys.ts * BHBC-1689: Fix validation for surveys. * BHBC-1689: Fix additional survey validation errors re publish_date. * BHBC-1689: Fix additional survey validation for purpose_and_methodolgy and proprietary_data. Co-authored-by: Anissa Agahchen Co-authored-by: Curtis Upshall * Add prettier organize imports plugin (#747) * Add prettier organize imports plugin * Run formatter * move permit endpoint functions to service (#751) * move permit endpoint functions to service * trigger openshift deploy * fix code smell * code smell v2 * Adjust elastic search query config to handle query terms with spaces (#752) * Adjust elastic search query config to handle query terms with spaces * Remove whole word search as it doesnt work, using this style anyways. * BHBC-1720: Added required fields indicators (#756) * BHBC-1720: Added asterisk to Project Boundary heading. * BHBC-1720: Add required indicator for surveyed_all_areas (not finalized). * BHBC-1720: Changed to 'Surveyed Areas *' * BHBC-1720: Update snapshot tests * Hide MU regions from the display list (#754) * removed MU regions list from the display * update api_delete_survey sql function (#757) * date picker fix (#753) * Bhbc-1725 fix deleting funding agency error (#758) * fix funding_source delete error * Bhbc 1728 (#760) Misc UI Clean-up Fixed the report year issue Fixing version to string * BHBC-1731: Fix open api spec (#762) Fix open api specs for survey-view, survey funding sources update, and project-view * Fix permit dropdown (#759) * remove the ability to select a non-sampling permit (when editing a project) * tweak openapi spec for public project view * More openapi fixes (#764) * public attachments received attachment/reports && secured/unsecured correctly * fixed api calls for retrieve signedUrl and to get unsecured files * Email to outlook (#765) * fixed email redirect in header * make agency_project_id nullable * remove permit from the public page * BHBC-1724: Fix edit dialog control clipping. (#761) Fix edit dialog control clipping * BHBC-1727: Add 'Zoom to Boundary Extent' button to SurveyStudyArea. (#763) * BHBC-1727: Add 'Zoom to Boundary Extent' button to SurveyStudyArea. * BHBC-1727: Automatically zoom to boundary extent upon rendering survey details. * BHBC-1727: make lint-fix; make format-fix; * BHBC-1727: Make Zoom to Boundary Extent button appear on top of the map * BHBC-1727: Fix codesmell. * BHBC-1727: Use absolute 'Zoom to boundary extent' IconButton in survey area map editor dialog. * BHBC-1727: Add 'Zoom to boundary extent' IconButton to Project Location map. * BHBC-1727: lint-fix and format-fit * BHBC-1727: Made PR requested changes. * BHBC-1727: Make lint-fix. * BHBC-1727: Update snapshot tests Co-authored-by: Anissa Agahchen * Fix user seed role id * Move to test (#767) * Update README.md * First commit (#566) * First commit * Upload * Update the sec produre for unseceruing an attachment * Version update * Update for single upload button * New fixtures * Update * Survey script * delete survey * Spacing * Update access request page: remove regional offices section, remove non-supported roles from dropdown (#610) * [BHBC-1449] Add Help Link (#614) * Update template validation schemas (#612) * Add migration to update moose SRB/Composition Validation schema * Add Summary Statistics file to resources page * Add Date column to recruitment survey * Fix dependency warnings * Remove unused test function * Update deployStatic.yml * BHBC-1363 (#599) Support project/survey report attachments with meta * Add admin only endpoint to update the api log level at runtime (#619) * Add admin only endpoint to update the api log level at runtime * Update test * Adding Test scripts (#617) * Adding Test scripts * Create project and Survey * Remove second test file * Test Updates/Fixes (#620) Test updates/Fixes * BHBC-1363-2: Fix signed url for reports, update fileupload to support single file only settings. (#621) * BHBC-1461 (#622) * Readme makefile updates (#623) * Update Makefile, Readme, env * Rename BioHub -> SIMS * Add `Order By` clause to species sql (#627) * BHBC-1462: Add new record to proprietary_type lookup table. (#629) - Update code to leverage the `is_first_nation` column from the proprietary_type table. - Previously the code was using the row id * BHBC-1470: View/edit survey and project metadata (#624) BHBC-1470: View/edit survey and project metadata (#624) * BHBC-1478: Update API to check project level permissions. (#630) * Combine dependabot (#634) * Bump tmpl from 1.0.4 to 1.0.5 in /app Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/daaku/nodejs-tmpl/releases) - [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5) --- updated-dependencies: - dependency-name: tmpl dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump ws from 5.2.2 to 5.2.3 in /app Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump validator from 13.6.0 to 13.7.0 in /n8n Bumps [validator](https://github.com/validatorjs/validator.js) from 13.6.0 to 13.7.0. - [Release notes](https://github.com/validatorjs/validator.js/releases) - [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/validatorjs/validator.js/compare/13.6.0...13.7.0) --- updated-dependencies: - dependency-name: validator dependency-type: indirect ... Signed-off-by: dependabot[bot] * Update Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * migration (#637) * Bhbc 1470 (#638) * Small test script and seeding changes (#631) * Small test script and seeding changes * Remove Shreyas from the seed Co-authored-by: Nick Phura * Create zap.yml * Update zap.yml * Create dev.context * Update zap.yml * Zap follow up (#640) * Small test script and seeding changes * Remove Shreyas from the seed * Details for ZAP * Subtitute hardcoded PW with GHA- Secret * [BHBC-1470] Increase Report Summary Input Size (#641) * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * Update zap.yml * BHBC-1467: One page project/survey layout (#636) One page project/survey layout * Test Script update, login and some other details (#646) * Test Script update, login and some other details * Add delay to login * Remove file * Pointing to the right authurl * Path confusion * And again * It is all in the timing * Update readme as per Nick's comment * BHBC-950 (#643) - System admin users can delete other system users. * Update DialogContext.Snackbar(#647) Update snackbar to accept ReactNode as message * Update deployStatic.yml * [BHBC-950] UI Cleanup (#648) * [BHBC-950] UI Cleanup * [BHBC-950] Updating UI * Trigger Build * Update to test script to deal with changed id for report upload * [BHBC-950] Fixing Request Access Table Co-authored-by: Roland Stens Co-authored-by: Anissa Agahchen * Update deployStatic.yml * BHBC-949: Manage Project Team - Add + Delete User (#645) - Add project users page - Add project users "add project participant" form - Add delete project user functionality - Update some api project role authorization checks - Update some frontend role check functionality - misc fixes/updates * Update deployStatic.yml * First test (#650) * First test * Update path * - update older node versions to 14 - Increase tsconfig versions - Increase a few package.json versions - Update a few git actions to latest versions * Revert accidental backup file change * Set back to es2015 otherwise the test framework will fail Co-authored-by: Nick Phura * Update deployStatic.yml * Manage system/project roles (#649) * BHBC-948: Allow users to change existing system user roles. * BHBC-1491: Allow users to change existing project participant's roles. * Update deployStatic.yml * [BHBC-948] UI Updates / Cleanup (#652) * Updates to Agancies and Proeject Coordinator name/label * Revert "Merge branch 'dev' of https://github.com/bcgov/biohubbc into dev" This reverts commit 16b5f48a44f0b791b25e7f2b36c0ede5d0db182f, reversing changes made to 1c88d34ead5ea84bdb03825e3a0667eb7de3e57b. * Changes to codes.ts for removing BC and all over for the coordinator vs contact name … (#654) * Cahnges to codes.ts and all over for the coordinator ro contact name change * Update snapshots * Finish BHBC-1505 * Update deployStatic.yml * BHBC-1457 (#651) Implemented EML endpoint and addition to target DwC archive file on S3. Moved all EML creation functionality to middle tier tooling. * Tech debt (#653) * Split up the workflow for uploading project/survey attachments and project/survey reports * associated tests * Updates to the test script and table changes (#658) * All changes * Update to formatting * Remove new table * Bhbc 1499 (#656) Updates to the Moose template and transformation configs * BHBC-1527: system and project roles updated (#659) * update url for download (#660) * Fix wonky survey view/view-for-update queries + misc (#664) * Fix survey view/view-for-update queries + misc * BHBC-1496: Transform Lat/Long to Darwin Core Accepted Format (#667) - Enhance transformations to allow multiple source columns in parse step (uses first non-null value) - Update scrape SQL to account for UTM or LatLong verbatimCoordinates - Add `parseLatLongString` spatial-util function to parse a lat long string. * BHBC-1525 (#669) * BHBC-1525 - initial EML test file and low hanging target testing * remove unused system accounts (#666) * remove unused system accounts * update code to reference previously updated role names * Security rules for PorH (#668) * Security rules for PorH * Endpoint Tests (#661) * Add missing `connection.commit`'s (#671) * Update endpoint role authorization (#670) * Update endpoint role authorization * Update occurrence scrape verbatimCoordinates handling * Bhbc 1400 (#657) Goat template validations and transformations Sheep template validations and transformations * UI Updates / Clean-up (#674) * Update all the BC and DC to tune the resource usage (#678) * BHBC-1523: Add new users (#675) * Added "+ new users" button on Admin user page and added testing for both API and app. * Bhbc 1529- Add custom props to templates (#677) * BHBC-1530: Update Moose transformations to account for all fields. (#680) * BHBC-1530 - Enhance xlsx transformation parse step - Update Moose validation/transformation schemas - Minor refactor to part of transformation code - Update resource page link * BHBC-1520: View user details (#679) - Ability to add system users from the manage users page - Associated endpoints and tests - Page to view user details and their associated projects - Ability to remove user from projects or update their role. - Associated endpoints and tests - Reduce some api code duplication * BHBC-1531 (#683) * BHBC-1531 - adjusted taxonomic codes to exclude those codes that have end date setc * Fix focal species yup schema validation rule * BHBC-1520 (#684) Prevent projects from having no Project Lead role: bug fixes + tests * BHBC-1512: Added zoom for fullscreen map on project details. (#685) Added zoom for fullscreen map on project details * Enhance useInterval hook to support a timeout. Add unit tests. (#686) * BHBC-1512-2: Additional map scroll wheel zoom changes (#687) * BHBC-1512-2: Additional map scroll wheel zoom changes * BHBC-1545: Tech Debt: Re-export all queries via queries.ts. Update custom error naming. (#688) Add index files for all query folders Add queries.ts to re-export all queries. Update all references to existing queries to instead reference queries.ts Rename `CustomError.ts->CustomError` to `custom-error.ts->HTTPError` and all references. Run formatter/linter. * BHBC-1545: Tech Debt: Updated apidoc for /user/* (#681) Update openapi doc for /user/* endpoints Misc unit test updates * BHBC-1503: GCNotfiy (#689) * Created GCNotify Api endpoint for generic email functionality * BHBC-1557: Tech Debt 3 (#690) * Add user-service.ts Move user functions to user-service.ts. Update tests. Add code-service.ts Move code functions to code-service.ts Update tests. Misc root-api-doc.ts cleanup Add swagger-ui container to docker-compose * Add SwaggerUI to docker-compose.yml (for local dev currently) * Minor makefile updates * Updates to error classes * Updates to error logging * Minor test tweak * Handle DatabaseErrors * remove console logs * BHBC-1492: Email Access Request (#692) * BHBC-1557: Tech Debt - Update App route handling and route security. (#693) * Tech debt: Fix partnerships, and update /project/{projectId}/update endpoint (#694) * BHBC-1557: Ke-tech-debt (#698) * updated openApi schemas * Added Survey creation. (#700) * Unify dependabot (#701) * Bump nanoid from 3.1.30 to 3.2.0 in /n8n (#704) * Bump copy-props from 2.0.4 to 2.0.5 in /api (#702) * Bump node-fetch from 2.6.5 to 2.6.7 in /n8n (#705) * BHBC-1557: Add ProjectService. (#699) * Bump lodash from 4.17.20 to 4.17.21 in /app (#703) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21) --- updated-dependencies: - dependency-name: lodash dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anissa Agahchen * BHBC-1493: Approval Email For Access Request (#708) Approval email sent to idir email connected to access request user. * Dev ops scale schemaspy up and down (#709) * Scale SchemaSpy as part of the pipeline to make sure it is current all the time * Make it a one step SchemaSpy cycle * Formatting * Fix typo * Reduce cpu/request limits * Revert "Reduce cpu/request limits" This reverts commit b99f8cbcfcd027e75c16370eb8afd9913102a15c. * Reduce cpu/request limits + Adjust default Log Level (#716) * BCEID bug fix + improvements (#717) * BHBC-1613 - support bceid login (#720) * BHBC-1580: Updates to moose/sheep/goat templates (#725) * Tech debt 6 (#707) * Disable html logging when running app tests. * Update logger utils, make mocha disable winston logger before test execution * Add missing type definition for @tmcw/togeojson * Updates to map components (from resto) * Update typescript and knex versions, remove swagger ui docker compose step, add swagger-ui directly to app * Incorporate dependabot changes * Update eslint config * Run lint-fix using updated eslint rules and fix issues * Update startend date component * Move old `/users` endpoint to `/user/list` * First cut of the postman collection with automatic login (#726) * Isolating Service Code (#724) Port changes from restoration to move functionality to services * Update siteminder url (#736) * Survey- Additional Fields (#731) * create/view/edit survey page with new purpose and methodology form * addition of "ecological season", "intended outcome" and "vantage" lookup tables & removal of common survey methodology - field table name change to "field method" * "survey.objectives" attribute name change to "survey.additional_details" * seeding new lookup tables * re-seed "field method" table * tests * package lock updates for minimist (#740) * dependabot change version for moment (#739) * Bump convict from 6.2.0 to 6.2.2 in /n8n (#745) Bumps [convict](https://github.com/mozilla/node-convict) from 6.2.0 to 6.2.2. - [Release notes](https://github.com/mozilla/node-convict/releases) - [Changelog](https://github.com/mozilla/node-convict/blob/master/CHANGELOG.md) - [Commits](https://github.com/mozilla/node-convict/compare/v6.2.0...v6.2.2) --- updated-dependencies: - dependency-name: convict dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump async from 2.6.3 to 2.6.4 in /app (#743) Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump async from 3.2.0 to 3.2.3 in /testing/e2e (#742) Bumps [async](https://github.com/caolan/async) from 3.2.0 to 3.2.3. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/master/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v3.2.0...v3.2.3) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * BHBC-1688: Add survey radio field (#746) * BHBC-1688: Add new radio button field to survey. * Add github action to write openshift urls * Techdebt (#741) remove default logs fix tests fix code smells * Misc Cleanup (#748) * misc cleanup * Update user seed file * BHBC-1690: Add scroll to error formik component (#749) * Scroll to survey form errors. * Fix vantage codes bug that returned an array with a null element (#750) * Sims taxonomy (#744) * app is working with the taxonomy service * updated snapshots * tests * survey service set up, using it for survey list * skipping broken tests * Remove `wldtaxonomic_units` table from survey queries * merge conflicts fixed * skipped some tests * add TODO comment * Trigger Build * Fix some tests * fixes attachmentList * fixed tests * Update project page test * surveysList test updated * Update ProjectsListPage.test.tsx * Update ReviewAccessRequestForm.test.tsx * test for Active Users List * fix more tests * code smells * Remove unused imports * fix general information bug * fix warning on general information form Co-authored-by: Nick Phura Co-authored-by: Kjartan * BHBC-1689: Implement API Response Validation (#737) * Add api response validation * Updates * update reponse for the project attachments list * BHBC-1689: Add validation to projects * BHBC-1689: Add response validation to surveys + attachments + observations * BHBC-1689: Add response validation to project attachments. * BHBC-1689: Fix publish_year type for report attachment uploading via POST. * BHBC-1689: Fix api response validation for user data response. * BHBC-1689: Change year_published type from string to number in attachment tests. * BHBC-1689: Update tests * Update occurrence submission response openapi * BHBC-1689: Fix api response validation for uploads * Update summary submission openapi and parse logic * BHBC-1689: Fix all failing tests for reports * BHBC-1689: Make lint-fix and make format-fix. * BHBC-1689: Remove console logs used for testing. * api validation for view.ts and surveys.ts * BHBC-1689: Fix validation for surveys. * BHBC-1689: Fix additional survey validation errors re publish_date. * BHBC-1689: Fix additional survey validation for purpose_and_methodolgy and proprietary_data. Co-authored-by: Anissa Agahchen Co-authored-by: Curtis Upshall * Add prettier organize imports plugin (#747) * Add prettier organize imports plugin * Run formatter * move permit endpoint functions to service (#751) * move permit endpoint functions to service * trigger openshift deploy * fix code smell * code smell v2 * Adjust elastic search query config to handle query terms with spaces (#752) * Adjust elastic search query config to handle query terms with spaces * Remove whole word search as it doesnt work, using this style anyways. * BHBC-1720: Added required fields indicators (#756) * BHBC-1720: Added asterisk to Project Boundary heading. * BHBC-1720: Add required indicator for surveyed_all_areas (not finalized). * BHBC-1720: Changed to 'Surveyed Areas *' * BHBC-1720: Update snapshot tests * Hide MU regions from the display list (#754) * removed MU regions list from the display * update api_delete_survey sql function (#757) * date picker fix (#753) * Bhbc-1725 fix deleting funding agency error (#758) * fix funding_source delete error * Bhbc 1728 (#760) Misc UI Clean-up Fixed the report year issue Fixing version to string * BHBC-1731: Fix open api spec (#762) Fix open api specs for survey-view, survey funding sources update, and project-view * Fix permit dropdown (#759) * remove the ability to select a non-sampling permit (when editing a project) * tweak openapi spec for public project view * More openapi fixes (#764) * public attachments received attachment/reports && secured/unsecured correctly * fixed api calls for retrieve signedUrl and to get unsecured files * Email to outlook (#765) * fixed email redirect in header * make agency_project_id nullable * remove permit from the public page * BHBC-1724: Fix edit dialog control clipping. (#761) Fix edit dialog control clipping * BHBC-1727: Add 'Zoom to Boundary Extent' button to SurveyStudyArea. (#763) * BHBC-1727: Add 'Zoom to Boundary Extent' button to SurveyStudyArea. * BHBC-1727: Automatically zoom to boundary extent upon rendering survey details. * BHBC-1727: make lint-fix; make format-fix; * BHBC-1727: Make Zoom to Boundary Extent button appear on top of the map * BHBC-1727: Fix codesmell. * BHBC-1727: Use absolute 'Zoom to boundary extent' IconButton in survey area map editor dialog. * BHBC-1727: Add 'Zoom to boundary extent' IconButton to Project Location map. * BHBC-1727: lint-fix and format-fit * BHBC-1727: Made PR requested changes. * BHBC-1727: Make lint-fix. * BHBC-1727: Update snapshot tests Co-authored-by: Anissa Agahchen * Fix user seed role id Co-authored-by: Nick Phura Co-authored-by: jeznorth Co-authored-by: Anissa Agahchen Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nick Phura Co-authored-by: Charlie Garrett-Jones <32178098+cgarrettjones@users.noreply.github.com> Co-authored-by: Kjartan <35311998+KjartanE@users.noreply.github.com> Co-authored-by: Shrey Patel <60997794+shreypdev@users.noreply.github.com> Co-authored-by: Kjartan Co-authored-by: Curtis Upshall * BHBC-1700: Move to Test (#774) * Fix/Enhancement: Port improved user access request functionality from Restoration, Adjust access request approval to account for existing bceid-basic-business identity sources (#771) - Port improved user access request functionality from Restoration - Better/fixed handling for approving/denying user requests - Adjust access request approval to account for existing bceid-basic-business identity sources - Existing access requests created before we accounted for bceid-basic-and-business identity sources should no longer throw an error when trying to approve/deny them. * BHBC-1730: Use centerOfMass instead of centroid for project area center (#770) * BHBC-1730: Use centerOfMass instead of centroid for project area center point. * BHBC-1736: Fix Read More/Read Less idempotence bug (#772) * BHBC-1736: Tighten qualification for string truncating * BHBC-1736: Fix null coalesce * BHBC-1736: Change variable name to be more descriptive Co-authored-by: Curtis Upshall * BHBC-1700: Move to Test (#777) * Bugfix: Incorrect identity source enum (#776) - account for the fact that openapi enums are case sensitive Co-authored-by: Roland Stens Co-authored-by: jeznorth Co-authored-by: Anissa Agahchen Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Charlie Garrett-Jones <32178098+cgarrettjones@users.noreply.github.com> Co-authored-by: Kjartan <35311998+KjartanE@users.noreply.github.com> Co-authored-by: Shrey Patel <60997794+shreypdev@users.noreply.github.com> Co-authored-by: Kjartan Co-authored-by: Curtis Upshall --- .config/config.json | 4 +- .github/pull_request_template.md | 30 +- .github/workflows/addComments.yml | 23 + .github/workflows/cleanClosedPR.yml | 4 +- .github/workflows/deploy.yml | 39 +- .github/workflows/deployStatic.yml | 78 +- .github/workflows/format.yml | 6 +- .github/workflows/lint.yml | 6 +- .github/workflows/test.yml | 6 +- .github/workflows/zap.yml | 27 + .sonarcloud.properties | 4 +- Makefile | 73 +- README.md | 19 +- api/.docker/api/Dockerfile | 2 +- api/.eslintrc | 9 + api/.mocharc.json | 2 +- api/.pipeline/config.js | 16 +- api/.pipeline/lib/api.deploy.js | 2 + api/.pipeline/package.json | 3 +- api/Dockerfile | 2 +- api/README.md | 7 +- api/mocha-fixtures.ts | 8 + api/openshift/api.bc.yaml | 10 +- api/openshift/api.dc.yaml | 97 +- api/package-lock.json | 15076 +++++++++- api/package.json | 87 +- api/src/__mocks__/db.ts | 52 +- api/src/app.ts | 122 +- api/src/constants/attachments.ts | 4 + api/src/constants/codes.ts | 260 +- api/src/constants/database.ts | 6 + api/src/constants/keycloak.ts | 4 + api/src/constants/notifications.ts | 19 + api/src/constants/roles.ts | 17 +- api/src/database/db.test.ts | 21 +- api/src/database/db.ts | 29 +- api/src/errors/CustomError.test.ts | 56 - api/src/errors/CustomError.ts | 60 - api/src/errors/custom-error.test.ts | 139 + api/src/errors/custom-error.ts | 215 + .../json-schema/transformation-schema.test.ts | 83 +- api/src/json-schema/transformation-schema.ts | 42 +- api/src/models/gcnotify.ts | 16 + api/src/models/project-create.test.ts | 8 +- api/src/models/project-create.ts | 2 +- .../models/project-survey-attachments.test.ts | 145 +- api/src/models/project-survey-attachments.ts | 69 +- api/src/models/project-update.test.ts | 801 +- api/src/models/project-update.ts | 235 +- api/src/models/project-view-update.test.ts | 56 - api/src/models/project-view-update.ts | 46 - api/src/models/project-view.test.ts | 386 +- api/src/models/project-view.ts | 198 +- api/src/models/public/project.test.ts | 79 +- api/src/models/public/project.ts | 41 +- api/src/models/survey-create.test.ts | 56 +- api/src/models/survey-create.ts | 18 +- api/src/models/survey-update.test.ts | 305 +- api/src/models/survey-update.ts | 106 +- api/src/models/survey-view-update.test.ts | 9 + api/src/models/survey-view-update.ts | 34 +- api/src/models/survey-view.test.ts | 121 +- api/src/models/survey-view.ts | 196 +- api/src/models/user.test.ts | 9 +- api/src/models/user.ts | 20 +- api/src/openapi/root-api-doc.ts | 99 +- .../schemas/administrative-activity.ts | 4 +- api/src/openapi/schemas/geoJson.ts | 437 + api/src/openapi/schemas/permit-no-sampling.ts | 2 +- api/src/openapi/schemas/project.test.ts | 11 +- api/src/openapi/schemas/project.ts | 9 - api/src/openapi/schemas/survey.test.ts | 20 - api/src/openapi/schemas/survey.ts | 140 - api/src/paths/access-request.test.ts | 176 - api/src/paths/access-request.ts | 177 - .../paths/administrative-activities.test.ts | 89 +- api/src/paths/administrative-activities.ts | 78 +- api/src/paths/administrative-activity.test.ts | 104 +- api/src/paths/administrative-activity.ts | 153 +- .../approve.test.ts | 116 + .../{administrativeActivityId}/approve.ts | 167 + .../{administrativeActivityId}/reject.test.ts | 66 + .../{administrativeActivityId}/reject.ts | 92 + api/src/paths/codes.test.ts | 17 +- api/src/paths/codes.ts | 136 +- api/src/paths/draft.test.ts | 53 +- api/src/paths/draft.ts | 48 +- api/src/paths/draft/{draftId}/delete.test.ts | 19 +- api/src/paths/draft/{draftId}/delete.ts | 36 +- api/src/paths/draft/{draftId}/get.test.ts | 11 +- api/src/paths/draft/{draftId}/get.ts | 26 +- api/src/paths/drafts.test.ts | 19 +- api/src/paths/drafts.ts | 24 +- api/src/paths/dwc/eml.test.ts | 658 + api/src/paths/dwc/eml.ts | 887 + api/src/paths/dwc/scrape-occurrences.ts | 27 +- api/src/paths/dwc/validate.test.ts | 35 +- api/src/paths/dwc/validate.ts | 50 +- api/src/paths/dwc/view-occurrences.test.ts | 27 +- api/src/paths/dwc/view-occurrences.ts | 38 +- api/src/paths/gcnotify/send.test.ts | 207 + api/src/paths/gcnotify/send.ts | 204 + api/src/paths/logger.test.ts | 59 + api/src/paths/logger.ts | 76 + .../paths/permit/create-no-sampling.test.ts | 200 +- api/src/paths/permit/create-no-sampling.ts | 76 +- api/src/paths/permit/get-no-sampling.test.ts | 125 +- api/src/paths/permit/get-no-sampling.ts | 53 +- api/src/paths/permit/list.test.ts | 131 +- api/src/paths/permit/list.ts | 53 +- api/src/paths/project.test.ts | 97 - api/src/paths/project.ts | 420 - api/src/paths/project/create.test.ts | 71 + api/src/paths/project/create.ts | 103 + api/src/paths/project/list.test.ts | 108 + .../paths/{projects.ts => project/list.ts} | 96 +- .../{projectId}/attachments/list.test.ts | 23 +- .../project/{projectId}/attachments/list.ts | 75 +- .../attachments/report/upload.test.ts | 146 + .../{projectId}/attachments/report/upload.ts | 329 + .../{projectId}/attachments/upload.test.ts | 66 +- .../project/{projectId}/attachments/upload.ts | 163 +- .../attachments/{attachmentId}/delete.test.ts | 58 +- .../attachments/{attachmentId}/delete.ts | 127 +- .../{attachmentId}/getSignedUrl.test.ts | 165 +- .../{attachmentId}/getSignedUrl.ts | 184 +- .../{attachmentId}/makeSecure.test.ts | 27 +- .../attachments/{attachmentId}/makeSecure.ts | 47 +- .../{attachmentId}/makeUnsecure.test.ts | 35 +- .../{attachmentId}/makeUnsecure.ts | 50 +- .../{attachmentId}/metadata/get.test.ts | 161 + .../{attachmentId}/metadata/get.ts | 197 + .../{attachmentId}/metadata/update.test.ts | 279 + .../{attachmentId}/metadata/update.ts | 212 + .../paths/project/{projectId}/delete.test.ts | 81 +- api/src/paths/project/{projectId}/delete.ts | 125 +- .../{projectId}/funding-sources/add.test.ts | 35 +- .../{projectId}/funding-sources/add.ts | 25 +- .../funding-sources/{pfsId}/delete.test.ts | 53 +- .../funding-sources/{pfsId}/delete.ts | 40 +- .../{projectId}/participants/create.test.ts | 83 + .../{projectId}/participants/create.ts | 155 + .../{projectId}/participants/get.test.ts | 77 + .../project/{projectId}/participants/get.ts | 137 + .../{projectParticipationId}/delete.test.ts | 194 + .../{projectParticipationId}/delete.ts | 148 + .../{projectParticipationId}/update.test.ts | 216 + .../{projectParticipationId}/update.ts | 155 + .../paths/project/{projectId}/publish.test.ts | 68 +- api/src/paths/project/{projectId}/publish.ts | 41 +- .../project/{projectId}/survey/create.test.ts | 148 +- .../project/{projectId}/survey/create.ts | 208 +- .../survey/funding-sources/list.test.ts | 21 +- .../survey/funding-sources/list.ts | 30 +- .../{projectId}/survey/permits/list.test.ts | 21 +- .../{projectId}/survey/permits/list.ts | 30 +- .../{surveyId}/attachments/list.test.ts | 23 +- .../survey/{surveyId}/attachments/list.ts | 72 +- .../attachments/report/upload.test.ts | 191 + .../{surveyId}/attachments/report/upload.ts | 357 + .../{surveyId}/attachments/upload.test.ts | 129 +- .../survey/{surveyId}/attachments/upload.ts | 163 +- .../attachments/{attachmentId}/delete.test.ts | 58 +- .../attachments/{attachmentId}/delete.ts | 114 +- .../{attachmentId}/getSignedUrl.test.ts | 165 +- .../{attachmentId}/getSignedUrl.ts | 166 +- .../{attachmentId}/makeSecure.test.ts | 31 +- .../attachments/{attachmentId}/makeSecure.ts | 30 +- .../{attachmentId}/makeUnsecure.test.ts | 39 +- .../{attachmentId}/makeUnsecure.ts | 30 +- .../{attachmentId}/metadata/get.test.ts | 179 + .../{attachmentId}/metadata/get.ts | 201 + .../{attachmentId}/metadata/update.test.ts | 324 + .../{attachmentId}/metadata/update.ts | 224 + .../survey/{surveyId}/delete.test.ts | 42 +- .../{projectId}/survey/{surveyId}/delete.ts | 29 +- .../observation/submission/get.test.ts | 37 +- .../{surveyId}/observation/submission/get.ts | 39 +- .../observation/submission/upload.test.ts | 606 +- .../observation/submission/upload.ts | 83 +- .../submission/{submissionId}/delete.test.ts | 29 +- .../submission/{submissionId}/delete.ts | 30 +- .../{submissionId}/getSignedUrl.test.ts | 29 +- .../submission/{submissionId}/getSignedUrl.ts | 41 +- .../submission/{submissionId}/view.test.ts | 37 +- .../submission/{submissionId}/view.ts | 43 +- .../survey/{surveyId}/publish.test.ts | 56 +- .../{projectId}/survey/{surveyId}/publish.ts | 45 +- .../{surveyId}/summary/submission/get.test.ts | 23 +- .../{surveyId}/summary/submission/get.ts | 36 +- .../summary/submission/upload.test.ts | 450 +- .../{surveyId}/summary/submission/upload.ts | 99 +- .../submission/{summaryId}/delete.test.ts | 29 +- .../summary/submission/{summaryId}/delete.ts | 28 +- .../{summaryId}/getSignedUrl.test.ts | 29 +- .../submission/{summaryId}/getSignedUrl.ts | 43 +- .../submission/{summaryId}/view.test.ts | 35 +- .../summary/submission/{summaryId}/view.ts | 41 +- .../survey/{surveyId}/update.test.ts | 771 +- .../{projectId}/survey/{surveyId}/update.ts | 558 +- .../survey/{surveyId}/view.test.ts | 409 +- .../{projectId}/survey/{surveyId}/view.ts | 424 +- .../paths/project/{projectId}/surveys.test.ts | 187 - api/src/paths/project/{projectId}/surveys.ts | 122 +- .../paths/project/{projectId}/update.test.ts | 215 +- api/src/paths/project/{projectId}/update.ts | 824 +- .../paths/project/{projectId}/view.test.ts | 435 +- api/src/paths/project/{projectId}/view.ts | 390 +- api/src/paths/projects.test.ts | 92 - api/src/paths/public/project/list.test.ts | 94 + api/src/paths/public/project/list.ts | 70 + .../{projectId}/attachments/list.test.ts | 39 +- .../project/{projectId}/attachments/list.ts | 60 +- .../{attachmentId}/getSignedUrl.test.ts | 174 +- .../{attachmentId}/getSignedUrl.ts | 137 +- .../{attachmentId}/metadata/get.test.ts | 161 + .../{attachmentId}/metadata/get.ts | 177 + .../public/project/{projectId}/view.test.ts | 72 + .../paths/public/project/{projectId}/view.ts | 369 +- api/src/paths/public/projects.test.ts | 102 - api/src/paths/public/projects.ts | 123 - api/src/paths/public/search.test.ts | 19 +- api/src/paths/public/search.ts | 9 +- api/src/paths/search.test.ts | 21 +- api/src/paths/search.ts | 28 +- api/src/paths/taxonomy/species/list.test.ts | 97 + api/src/paths/taxonomy/species/list.ts | 87 + api/src/paths/taxonomy/species/search.test.ts | 97 + api/src/paths/taxonomy/species/search.ts | 85 + api/src/paths/user.test.ts | 266 - api/src/paths/user.ts | 151 - api/src/paths/user/add.test.ts | 143 + api/src/paths/user/add.ts | 130 + api/src/paths/user/list.test.ts | 44 + api/src/paths/{users.ts => user/list.ts} | 58 +- api/src/paths/user/self.test.ts | 143 +- api/src/paths/user/self.ts | 70 +- api/src/paths/user/{userId}/delete.test.ts | 700 + api/src/paths/user/{userId}/delete.ts | 250 + api/src/paths/user/{userId}/get.test.ts | 97 + api/src/paths/user/{userId}/get.ts | 140 + .../paths/user/{userId}/projects/get.test.ts | 120 + api/src/paths/user/{userId}/projects/get.ts | 173 + .../paths/user/{userId}/system-roles.test.ts | 385 - api/src/paths/user/{userId}/system-roles.ts | 260 - .../user/{userId}/system-roles/create.test.ts | 216 + .../user/{userId}/system-roles/create.ts | 138 + .../user/{userId}/system-roles/update.test.ts | 245 + .../user/{userId}/system-roles/update.ts | 136 + api/src/paths/users.test.ts | 94 - api/src/paths/version.ts | 6 +- api/src/paths/xlsx/transform.test.ts | 60 - api/src/paths/xlsx/transform.ts | 36 +- api/src/paths/xlsx/validate.test.ts | 113 +- api/src/paths/xlsx/validate.ts | 59 +- .../administrative-activity-queries.test.ts | 35 +- .../administrative-activity-queries.ts | 148 +- .../queries/administrative-activity/index.ts | 3 + api/src/queries/codes/code-queries.test.ts | 18 +- api/src/queries/codes/code-queries.ts | 74 +- api/src/queries/codes/db-constant-queries.ts | 13 + api/src/queries/codes/index.ts | 4 + api/src/queries/database/index.ts | 3 + .../user-context-queries.test.ts | 2 +- .../queries/database/user-context-queries.ts | 13 + api/src/queries/dwc/dwc-queries.ts | 458 + api/src/queries/dwc/index.ts | 3 + api/src/queries/occurrence/index.ts | 4 + .../occurrence/occurrence-create-queries.ts | 37 +- .../occurrence/occurrence-view-queries.ts | 16 - api/src/queries/permit/index.ts | 5 + .../permit/permit-create-queries.test.ts | 2 +- .../queries/permit/permit-create-queries.ts | 37 +- .../queries/permit/permit-update-queries.ts | 21 +- .../permit/permit-view-queries.test.ts | 2 +- api/src/queries/permit/permit-view-queries.ts | 37 +- .../queries/project-participation/index.ts | 3 + .../project-participation-queries.test.ts | 97 + .../project-participation-queries.ts | 246 + .../{ => project/draft}/draft-queries.test.ts | 0 .../{ => project/draft}/draft-queries.ts | 48 - api/src/queries/project/draft/index.ts | 3 + api/src/queries/project/index.ts | 15 + .../project-attachments-queries.test.ts | 236 +- .../project/project-attachments-queries.ts | 378 +- .../queries/project/project-create-queries.ts | 86 +- .../project/project-delete-queries.test.ts | 6 +- .../queries/project/project-delete-queries.ts | 95 - .../project/project-update-queries.test.ts | 10 +- .../queries/project/project-update-queries.ts | 130 +- .../project/project-view-queries.test.ts | 68 +- .../queries/project/project-view-queries.ts | 212 +- .../project-view-update-queries.test.ts | 64 - .../project/project-view-update-queries.ts | 166 - api/src/queries/public/index.ts | 4 + .../queries/public/project-queries.test.ts | 134 +- api/src/queries/public/project-queries.ts | 466 +- api/src/queries/public/search-queries.ts | 16 +- api/src/queries/queries.ts | 31 + api/src/queries/search/index.ts | 3 + .../{ => search}/search-queries.test.ts | 0 .../queries/{ => search}/search-queries.ts | 12 - api/src/queries/security/index.ts | 3 + .../queries/security/security-queries.test.ts | 2 +- api/src/queries/security/security-queries.ts | 21 - .../generate-geometry-collection.ts | 2 +- api/src/queries/spatial/index.ts | 3 + api/src/queries/survey/index.ts | 19 + .../survey/survey-attachments-queries.test.ts | 251 +- .../survey/survey-attachments-queries.ts | 373 +- .../survey/survey-create-queries.test.ts | 8 +- .../queries/survey/survey-create-queries.ts | 121 +- .../survey/survey-delete-queries.test.ts | 8 +- .../queries/survey/survey-delete-queries.ts | 102 +- .../survey/survey-occurrence-queries.test.ts | 52 +- .../survey/survey-occurrence-queries.ts | 242 +- .../survey/survey-summary-queries.test.ts | 8 +- .../queries/survey/survey-summary-queries.ts | 249 +- .../survey/survey-update-queries.test.ts | 19 +- .../queries/survey/survey-update-queries.ts | 129 +- .../survey/survey-view-queries.test.ts | 76 +- api/src/queries/survey/survey-view-queries.ts | 238 +- .../survey/survey-view-update-queries.test.ts | 22 +- .../survey/survey-view-update-queries.ts | 146 +- api/src/queries/user-context-queries.ts | 27 - api/src/queries/users/index.ts | 4 + .../queries/users/system-role-queries.test.ts | 28 +- api/src/queries/users/system-role-queries.ts | 55 +- api/src/queries/users/user-queries.test.ts | 73 +- api/src/queries/users/user-queries.ts | 184 +- .../security/authentication.test.ts | 80 + .../security/authentication.ts | 131 + .../security/authorization.test.ts | 787 + .../security/authorization.ts | 390 + api/src/security/auth-utils.test.ts | 383 - api/src/security/auth-utils.ts | 249 - api/src/services/code-service.test.ts | 52 + api/src/services/code-service.ts | 128 + api/src/services/gcnotify-service.test.ts | 116 + api/src/services/gcnotify-service.ts | 80 + api/src/services/keycloak-service.test.ts | 158 + api/src/services/keycloak-service.ts | 137 + api/src/services/permit-service.test.ts | 313 + api/src/services/permit-service.ts | 124 + api/src/services/project-service.test.ts | 509 + api/src/services/project-service.ts | 1175 + api/src/services/service.ts | 15 + api/src/services/survey-service.ts | 101 + api/src/services/taxonomy-service.test.ts | 14 + api/src/services/taxonomy-service.ts | 99 + api/src/services/user-service.test.ts | 630 + api/src/services/user-service.ts | 228 + api/src/utils/code-utils.ts | 116 - api/src/utils/db-constant-utils.ts | 25 + api/src/utils/file-utils.ts | 2 +- api/src/utils/keycloak-utils.test.ts | 116 +- api/src/utils/keycloak-utils.ts | 31 +- api/src/utils/logger.test.ts | 65 +- api/src/utils/logger.ts | 65 +- .../validation/csv-header-validator.test.ts | 4 +- .../csv/validation/csv-row-validator.test.ts | 6 +- api/src/utils/media/media-file.ts | 18 +- .../validation/validation-schema-parser.ts | 4 +- .../transformation-schema-parser.ts | 15 +- .../transformation/xlsx-transformation.ts | 137 +- api/src/utils/path-utils.ts | 20 - api/src/utils/shared-api-docs.test.ts | 4 +- api/src/utils/shared-api-docs.ts | 17 +- api/src/utils/spatial-utils.test.ts | 69 +- api/src/utils/spatial-utils.ts | 73 +- api/tsconfig.json | 4 +- app/.docker/app/Dockerfile | 2 +- app/.eslintrc | 27 +- app/.gitignore | 1 - app/.pipeline/config.js | 9 + app/.pipeline/lib/app.deploy.js | 2 + app/.pipeline/package.json | 3 +- app/Dockerfile | 4 +- app/README.md | 11 +- app/openshift/app.bc.yaml | 12 +- app/openshift/app.dc.yaml | 30 +- app/package-lock.json | 22767 +++++++++++++++- app/package.json | 32 +- app/server/index.js | 6 +- app/src/AppRouter.tsx | 167 +- .../attachments/AttachmentsList.test.tsx | 18 +- .../attachments/AttachmentsList.tsx | 370 +- app/src/components/attachments/DropZone.tsx | 107 +- .../attachments/EditFileWithMeta.tsx | 18 + .../attachments/EditReportMetaForm.tsx | 187 + app/src/components/attachments/FileUpload.tsx | 107 +- .../components/attachments/FileUploadItem.tsx | 159 +- .../attachments/FileUploadWithMeta.tsx | 67 + .../components/attachments/ReportMetaForm.tsx | 188 + .../__snapshots__/DropZone.test.tsx.snap | 68 +- .../boundary/FullScreenViewMapDialog.tsx | 65 + .../boundary/InferredLocationDetails.tsx | 47 + app/src/components/boundary/MapBoundary.tsx | 185 +- app/src/components/chips/RequestChips.tsx | 40 + app/src/components/dialog/ComponentDialog.tsx | 14 +- app/src/components/dialog/EditDialog.tsx | 19 +- .../dialog/EditFileWithMetaDialog.tsx | 144 + .../dialog/FileUploadWithMetaDialog.tsx | 162 + app/src/components/dialog/RequestDialog.tsx | 2 + .../dialog/ViewFileWithMetaDialog.tsx | 108 + .../components/dialog/YesNoDialog.test.tsx | 5 +- app/src/components/dialog/YesNoDialog.tsx | 66 +- .../__snapshots__/EditDialog.test.tsx.snap | 2 + .../__snapshots__/YesNoDialog.test.tsx.snap | 28 +- .../components/fields/AutocompleteField.tsx | 9 +- .../fields/AutocompleteFreeSoloField.tsx | 25 +- app/src/components/fields/CustomTextField.tsx | 12 +- .../fields/DollarAmountField.test.tsx | 14 +- .../components/fields/DollarAmountField.tsx | 7 +- .../HorizontalSplitFormComponent.test.tsx | 6 +- .../fields/HorizontalSplitFormComponent.tsx | 8 +- .../fields/MultiAutocompleteField.test.tsx | 2 +- .../fields/MultiAutocompleteField.tsx | 7 +- .../MultiAutocompleteFieldVariableSize.tsx | 158 +- app/src/components/fields/ReadMoreField.tsx | 12 +- .../components/fields/SelectWithSubtext.tsx | 107 + .../components/fields/StartEndDateFields.tsx | 39 +- .../DollarAmountField.test.tsx.snap | 32 +- ...HorizontalSplitFormComponent.test.tsx.snap | 18 +- .../components/formik/ScrollToFormikError.tsx | 105 + app/src/components/layout/Footer.tsx | 35 +- app/src/components/layout/Header.test.tsx | 32 +- app/src/components/layout/Header.tsx | 195 +- .../layout/__snapshots__/Footer.test.tsx.snap | 70 +- app/src/components/map/MapContainer.test.tsx | 12 +- app/src/components/map/MapContainer.tsx | 62 +- .../components/map/OccurrenceFeatureGroup.tsx | 6 +- .../__snapshots__/MapContainer.test.tsx.snap | 20 +- .../search-filter/ProjectAdvancedFilters.tsx | 57 +- app/src/components/security/Guards.tsx | 88 + app/src/components/security/RouteGuards.tsx | 222 + .../components/surveys/SurveysList.test.tsx | 40 +- app/src/components/surveys/SurveysList.tsx | 55 +- app/src/components/toolbar/ActionToolbars.tsx | 252 + app/src/constants/attachments.ts | 8 +- app/src/constants/i18n.ts | 90 +- app/src/constants/roles.ts | 17 +- app/src/contexts/configContext.tsx | 6 +- app/src/contexts/dialogContext.tsx | 38 +- app/src/features/admin/AdminUsersRouter.tsx | 29 +- .../admin/users/AccessRequestList.test.tsx | 86 +- .../admin/users/AccessRequestList.tsx | 125 +- .../admin/users/ActiveUsersList.test.tsx | 74 +- .../features/admin/users/ActiveUsersList.tsx | 389 +- .../admin/users/AddSystemUsersForm.tsx | 166 + .../admin/users/ManageUsersPage.test.tsx | 18 +- .../features/admin/users/ManageUsersPage.tsx | 21 +- .../users/ReviewAccessRequestForm.test.tsx | 147 +- .../admin/users/ReviewAccessRequestForm.tsx | 70 +- .../admin/users/UsersDetailHeader.test.tsx | 153 + .../admin/users/UsersDetailHeader.tsx | 180 + .../admin/users/UsersDetailPage.test.tsx | 82 + .../features/admin/users/UsersDetailPage.tsx | 60 + .../admin/users/UsersDetailProjects.test.tsx | 458 + .../admin/users/UsersDetailProjects.tsx | 380 + .../components/ObservationSubmissionCSV.tsx | 36 +- .../permits/CreatePermitPage.test.tsx | 6 +- app/src/features/permits/CreatePermitPage.tsx | 18 +- app/src/features/permits/PermitsPage.test.tsx | 12 +- app/src/features/permits/PermitsPage.tsx | 10 +- app/src/features/permits/PermitsRouter.tsx | 29 +- .../CreatePermitPage.test.tsx.snap | 106 +- .../__snapshots__/PermitsPage.test.tsx.snap | 6 +- app/src/features/projects/ProjectsLayout.tsx | 3 +- app/src/features/projects/ProjectsRouter.tsx | 156 +- .../projects/PublicProjectsRouter.tsx | 44 +- .../ProjectCoordinatorForm.test.tsx | 2 +- .../components/ProjectCoordinatorForm.tsx | 6 +- .../components/ProjectDetailsForm.tsx | 10 +- .../projects/components/ProjectDraftForm.tsx | 2 +- .../components/ProjectFundingForm.test.tsx | 6 +- .../components/ProjectFundingForm.tsx | 2 +- .../components/ProjectFundingItemForm.tsx | 18 +- .../components/ProjectIUCNForm.test.tsx | 16 +- .../projects/components/ProjectIUCNForm.tsx | 6 +- .../components/ProjectLocationForm.tsx | 19 +- .../components/ProjectObjectivesForm.test.tsx | 2 +- .../components/ProjectPermitForm.test.tsx | 2 +- .../projects/components/ProjectPermitForm.tsx | 14 +- .../ProjectCoordinatorForm.test.tsx.snap | 22 +- .../ProjectDetailsForm.test.tsx.snap | 4 + .../ProjectDraftForm.test.tsx.snap | 3 + .../ProjectFundingItemForm.test.tsx.snap | 30 +- .../ProjectIUCNForm.test.tsx.snap | 860 - .../ProjectLocationForm.test.tsx.snap | 206 +- .../ProjectObjectivesForm.test.tsx.snap | 4 + .../ProjectPartnershipsForm.test.tsx.snap | 4 + .../ProjectPermitForm.test.tsx.snap | 517 +- .../create/CreateProjectPage.test.tsx | 6 +- .../projects/create/CreateProjectPage.tsx | 10 +- .../CreateProjectPage.test.tsx.snap | 13 +- .../projects/list/ProjectsListPage.test.tsx | 78 +- .../projects/list/ProjectsListPage.tsx | 71 +- .../AddProjectParticipantsForm.tsx | 172 + .../ProjectParticipantsHeader.tsx | 151 + .../participants/ProjectParticipantsPage.tsx | 402 + .../projects/view/ProjectAttachments.test.tsx | 89 +- .../projects/view/ProjectAttachments.tsx | 128 +- .../features/projects/view/ProjectDetails.tsx | 67 +- .../features/projects/view/ProjectHeader.tsx | 302 + .../projects/view/ProjectPage.test.tsx | 75 +- .../features/projects/view/ProjectPage.tsx | 321 +- .../ProjectDetails.test.tsx.snap | 1142 +- .../__snapshots__/ProjectPage.test.tsx.snap | 4533 ++- .../view/components/FundingSource.test.tsx | 14 +- .../view/components/FundingSource.tsx | 190 +- .../components/GeneralInformation.test.tsx | 4 +- .../view/components/GeneralInformation.tsx | 27 +- .../components/IUCNClassification.test.tsx | 12 +- .../view/components/IUCNClassification.tsx | 67 +- .../view/components/LocationBoundary.test.tsx | 32 +- .../view/components/LocationBoundary.tsx | 159 +- .../view/components/Partnerships.test.tsx | 10 +- .../projects/view/components/Partnerships.tsx | 52 +- .../components/ProjectCoordinator.test.tsx | 26 +- .../view/components/ProjectCoordinator.tsx | 25 +- .../components/ProjectObjectives.test.tsx | 8 +- .../view/components/ProjectObjectives.tsx | 46 +- .../view/components/ProjectPermits.tsx | 123 +- .../__snapshots__/FundingSource.test.tsx.snap | 321 +- .../GeneralInformation.test.tsx.snap | 198 +- .../IUCNClassification.test.tsx.snap | 196 +- .../LocationBoundary.test.tsx.snap | 494 +- .../__snapshots__/Partnerships.test.tsx.snap | 69 +- .../ProjectCoordinator.test.tsx.snap | 66 +- .../ProjectObjectives.test.tsx.snap | 784 +- .../ProjectPermits.test.tsx.snap | 198 +- app/src/features/resources/ResourcesPage.tsx | 33 +- .../features/resources/ResourcesRouter.tsx | 22 +- app/src/features/search/SearchPage.test.tsx | 4 +- app/src/features/search/SearchPage.tsx | 19 +- .../surveys/CreateSurveyPage.test.tsx | 123 +- app/src/features/surveys/CreateSurveyPage.tsx | 137 +- .../CreateSurveyPage.test.tsx.snap | 2056 +- .../components/AgreementsForm.test.tsx | 2 +- .../surveys/components/AgreementsForm.tsx | 10 +- .../GeneralInformationForm.test.tsx | 70 +- .../components/GeneralInformationForm.tsx | 365 +- .../components/ProprietaryDataForm.test.tsx | 12 +- .../components/ProprietaryDataForm.tsx | 331 +- .../components/PurposeAndMethodologyForm.tsx | 147 + .../surveys/components/StudyAreaForm.test.tsx | 2 +- .../surveys/components/StudyAreaForm.tsx | 48 +- .../GeneralInformationForm.test.tsx.snap | 2289 +- .../ProprietaryDataForm.test.tsx.snap | 138 +- .../__snapshots__/StudyAreaForm.test.tsx.snap | 2176 +- .../surveys/list/SurveysListPage.test.tsx | 42 +- .../features/surveys/list/SurveysListPage.tsx | 37 +- .../surveys/view/SurveyAttachments.test.tsx | 165 +- .../surveys/view/SurveyAttachments.tsx | 138 +- .../surveys/view/SurveyDetails.test.tsx | 4 +- .../features/surveys/view/SurveyDetails.tsx | 57 +- .../features/surveys/view/SurveyHeader.tsx | 290 + .../surveys/view/SurveyObservations.test.tsx | 8 +- .../surveys/view/SurveyObservations.tsx | 253 +- .../features/surveys/view/SurveyPage.test.tsx | 37 +- app/src/features/surveys/view/SurveyPage.tsx | 310 +- .../surveys/view/SurveySummaryResults.tsx | 180 +- .../__snapshots__/SurveyDetails.test.tsx.snap | 786 +- .../__snapshots__/SurveyPage.test.tsx.snap | 2691 +- .../SurveyGeneralInformation.test.tsx | 42 +- .../components/SurveyGeneralInformation.tsx | 194 +- .../components/SurveyProprietaryData.test.tsx | 8 +- .../view/components/SurveyProprietaryData.tsx | 49 +- .../SurveyPurposeAndMethodology.tsx | 280 + .../view/components/SurveyStudyArea.test.tsx | 11 +- .../view/components/SurveyStudyArea.tsx | 164 +- .../SurveyGeneralInformation.test.tsx.snap | 646 +- .../SurveyProprietaryData.test.tsx.snap | 102 +- .../SurveyStudyArea.test.tsx.snap | 186 +- app/src/hooks/api/useAdminApi.test.ts | 46 +- app/src/hooks/api/useAdminApi.ts | 110 +- app/src/hooks/api/useAxios.test.ts | 6 +- app/src/hooks/api/useAxios.ts | 15 +- app/src/hooks/api/useN8NApi.test.ts | 51 + app/src/hooks/api/useN8NApi.ts | 29 +- app/src/hooks/api/useObservationApi.test.ts | 19 +- app/src/hooks/api/useObservationApi.ts | 28 +- app/src/hooks/api/useProjectApi.test.ts | 142 +- app/src/hooks/api/useProjectApi.ts | 262 +- app/src/hooks/api/useSurveyApi.test.ts | 76 +- app/src/hooks/api/useSurveyApi.ts | 156 +- app/src/hooks/api/useTaxonomyApi.test.ts | 53 + app/src/hooks/api/useTaxonomyApi.ts | 27 + app/src/hooks/api/useUserApi.test.ts | 30 +- app/src/hooks/api/useUserApi.ts | 54 +- app/src/hooks/useBioHubApi.ts | 14 +- app/src/hooks/useCodes.ts | 43 + app/src/hooks/useInterval.test.tsx | 101 + app/src/hooks/useInterval.ts | 41 +- app/src/hooks/useIsMounted.tsx | 2 +- app/src/hooks/useKeycloakWrapper.tsx | 42 +- app/src/index.tsx | 2 +- app/src/interfaces/useAdminApi.interface.ts | 33 +- app/src/interfaces/useCodesApi.interface.ts | 9 +- app/src/interfaces/useProjectApi.interface.ts | 74 +- app/src/interfaces/useSurveyApi.interface.ts | 83 +- app/src/interfaces/useUserApi.interface.ts | 1 + app/src/layouts/PublicLayout.test.tsx | 4 +- app/src/pages/200/RequestSubmitted.test.tsx | 2 +- app/src/pages/403/AccessDenied.test.tsx | 2 +- app/src/pages/404/NotFoundPage.tsx | 8 +- .../pages/access/AccessRequestPage.test.tsx | 71 +- app/src/pages/access/AccessRequestPage.tsx | 176 +- .../pages/access/BCeIDRequestForm.test.tsx | 5 +- app/src/pages/access/BCeIDRequestForm.tsx | 16 +- app/src/pages/access/IDIRRequestForm.tsx | 146 +- .../AccessRequestPage.test.tsx.snap | 187 +- .../BCeIDRequestForm.test.tsx.snap | 106 - app/src/pages/logout/LogOutPage.test.tsx | 2 +- app/src/pages/logout/LogOutPage.tsx | 2 +- .../public/PublicProjectDetails.test.tsx | 5 +- app/src/pages/public/PublicProjectDetails.tsx | 27 +- .../pages/public/PublicProjectPage.test.tsx | 24 +- app/src/pages/public/PublicProjectPage.tsx | 20 +- .../public/PublicProjectsListPage.test.tsx | 8 +- .../pages/public/PublicProjectsListPage.tsx | 18 +- .../PublicProjectDetails.test.tsx.snap | 170 +- .../PublicProjectPage.test.tsx.snap | 24 - .../components/PublicAttachmentsList.test.tsx | 69 +- .../components/PublicAttachmentsList.tsx | 190 +- .../PublicGeneralInformation.test.tsx | 3 +- .../components/PublicGeneralInformation.tsx | 15 +- .../PublicIUCNClassification.test.tsx | 3 +- .../components/PublicIUCNClassification.tsx | 23 +- .../components/PublicLocationBoundary.tsx | 23 +- .../components/PublicPartnerships.test.tsx | 3 +- .../public/components/PublicPartnerships.tsx | 10 +- .../components/PublicProjectCoordinator.tsx | 2 +- .../components/PublicProjectPermits.tsx | 4 +- .../PublicGeneralInformation.test.tsx.snap | 4 +- .../PublicIUCNClassification.test.tsx.snap | 20 +- .../PublicLocationBoundary.test.tsx.snap | 26 +- .../PublicPartnerships.test.tsx.snap | 6 +- .../PublicProjectCoordinator.test.tsx.snap | 2 +- app/src/setupTests.ts | 29 +- app/src/styles.scss | 30 +- app/src/test-helpers/auth-helpers.ts | 48 + app/src/test-helpers/code-helpers.ts | 42 +- app/src/test-helpers/project-helpers.ts | 16 +- app/src/test-helpers/survey-helpers.ts | 17 +- app/src/themes/appTheme.ts | 52 +- app/src/types/@tmcw/togeojson.d.ts | 12 + app/src/types/misc.ts | 1 + app/src/types/yup.d.ts | 25 + app/src/utils/AppRoute.tsx | 68 +- app/src/utils/MapEditControls.test.tsx | 6 +- app/src/utils/MapEditControls.tsx | 10 +- app/src/utils/PrivateRoute.tsx | 56 - app/src/utils/ProjectStepComponents.test.tsx | 2 +- app/src/utils/Utils.ts | 4 +- app/src/utils/YupSchema.ts | 49 +- .../ProjectStepComponents.test.tsx.snap | 55 +- app/src/utils/mapBoundaryUploadHelpers.ts | 101 +- app/src/utils/mapLayersHelpers.ts | 10 +- app/tsconfig.json | 2 +- .../metabase/Metabase.postman_collection.json | 369 + containers/n8n/n8n.dc.yaml | 8 +- database/.docker/db/Dockerfile.migrate | 2 +- database/.docker/db/Dockerfile.rollback | 2 +- database/.docker/db/Dockerfile.setup | 2 +- database/.pipeline/lib/db.deploy.js | 3 +- database/.pipeline/package.json | 3 +- database/README.md | 2 +- database/openshift/db.dc.yaml | 14 +- database/openshift/db.setup.bc.yaml | 8 +- database/openshift/db.setup.dc.yaml | 14 +- database/package-lock.json | 5193 +++- database/package.json | 25 +- .../20210225205948_biohub_release.ts | 2 +- .../20210715170001_security_tables.ts | 2 +- .../20210715170002_security_procedures.ts | 2 +- .../migrations/20210715170004_adding_roles.ts | 2 +- .../20210715170005_adding_system_rules.ts | 2 +- .../20210825170006_add_n8n_schema.ts | 2 +- .../20210909000000_validation_schemas.ts | 2 +- ...09150006_add_trigger_to_secured_objects.ts | 2 +- .../20210927010804_transformation_schema.ts | 2 +- .../20211015105029_validation_schemas_2.ts | 2 +- ...20211015106029_transformation_schemas_2.ts | 2 +- .../20211018010922_update_funding_source.ts | 2 +- ...0211020095230_update_validation_schemas.ts | 2 +- .../20211104203420_update_proprietor_type.ts | 30 + ...108115529_populate_summary_message_type.ts | 31 + ...095230_update_system_metadata_constants.ts | 19 + ...00_update_report_attachment_description.ts | 37 + .../20211123095231_survey_status_view.ts | 68 + ...123095232_drop_api_get_eml_data_package.ts | 15 + ...update_transformation_validation_schema.ts | 132 + .../20211129180001_security_rule update.ts | 22 + ...0_insert_goat_validation_transformation.ts | 102 + ..._insert_sheep_validation_transformation.ts | 102 + ...11206105001_update_system_project_roles.ts | 24 + .../20211206105010_add_security_rules.ts | 57 + ...1206121200_update_transformation_schema.ts | 111 + .../20211207105001_remove_user_roles.ts | 57 + ...1207110001_security_token_to_occurrence.ts | 24 + .../20211207110002_update_secure_record.ts | 135 + .../20211207170011_user_group_table.ts | 805 + ...216095022_update_moose_template_schemas.ts | 132 + ...20217010922_update_user_identity_source.ts | 24 + ...20223010922_revert_user_identity_source.ts | 24 + ...224160300_update_moose_template_schemas.ts | 120 + ...0228103100_update_goat_template_schemas.ts | 124 + ..._update_sheep_validation_transformation.ts | 126 + .../migrations/20220404093900_SIMS.1.1.0.ts | 452 + ...0220419152015_add_surveyed_areas_column.ts | 62 + ...20220428150001_update_api_delete_survey.ts | 67 + .../smoketest_release.1.0.0.sql} | 1 - .../smoke_tests/smoketest_release.1.1.0.sql | 241 + .../goat_composition_or_recruitment_1.json | 305 + .../goat_composition_or_recruitment_2.json | 308 + .../moose_srb_or_composition_survey_3.json | 679 + .../moose_srb_or_composition_survey_4.json | 719 + .../moose_srb_or_composition_survey_5.json | 2988 ++ .../moose_srb_or_composition_survey_6.json | 2988 ++ .../sheep_composition_or_recruitment_1.json | 535 + .../sheep_composition_or_recruitment_2.json | 446 + .../goat_composition_or_recruitment_1.json | 932 + .../goat_composition_or_recruitment_2.ts | 511 + .../moose_srb_or_composition_survey_4.json | 939 + .../moose_srb_or_composition_survey_5.json | 938 + .../moose_srb_or_composition_survey_6.ts | 528 + .../picklist_variables/v0.1.ts | 334 + .../sheep_composition_or_recruitment_1.json | 1022 + .../sheep_composition_or_recruitment_2.json | 1121 + .../sheep_composition_or_recruitment_3.ts | 534 + database/src/seeds/01_db_system_users.ts | 82 +- database/tsconfig.json | 4 +- docker-compose.yml | 20 +- env_config/env.docker | 34 +- n8n/package-lock.json | 160 +- n8n/package.json | 12 +- n8n/workflows/1.json | 6 +- n8n/workflows/2.json | 4 +- n8n/workflows/3.json | 4 +- n8n/workflows/4.json | 15 +- testing/e2e/cypress.json | 3 +- testing/e2e/cypress/README.md | 31 +- testing/e2e/cypress/fixtures/1.doc | Bin 0 -> 1416704 bytes testing/e2e/cypress/fixtures/1.xlsx | Bin 0 -> 78248 bytes .../smoke_1_CreateProjectMinimal.spec.ts | 18 +- .../page-functions/common/login-page.ts | 15 +- .../project/project-create-page.ts | 165 +- testing/e2e/cypress/support/commands.ts | 86 +- testing/e2e/cypress/support/createUUID.ts | 14 - testing/e2e/cypress/support/keycloak.js | 9 + testing/e2e/package-lock.json | 132 +- testing/e2e/package.json | 4 +- testing/e2e/tsconfig.json | 2 +- .../BioHubBC-API-DEV.postman_collection.json | 266 +- testing/zap/README.md | 40 + testing/zap/dev.context | 85 + 758 files changed, 114745 insertions(+), 29842 deletions(-) create mode 100644 .github/workflows/addComments.yml create mode 100644 .github/workflows/zap.yml create mode 100644 api/mocha-fixtures.ts create mode 100644 api/src/constants/attachments.ts create mode 100644 api/src/constants/keycloak.ts create mode 100644 api/src/constants/notifications.ts delete mode 100644 api/src/errors/CustomError.test.ts delete mode 100644 api/src/errors/CustomError.ts create mode 100644 api/src/errors/custom-error.test.ts create mode 100644 api/src/errors/custom-error.ts create mode 100644 api/src/models/gcnotify.ts delete mode 100644 api/src/models/project-view-update.test.ts delete mode 100644 api/src/models/project-view-update.ts create mode 100644 api/src/openapi/schemas/geoJson.ts delete mode 100644 api/src/openapi/schemas/survey.test.ts delete mode 100644 api/src/openapi/schemas/survey.ts delete mode 100644 api/src/paths/access-request.test.ts delete mode 100644 api/src/paths/access-request.ts create mode 100644 api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.test.ts create mode 100644 api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.ts create mode 100644 api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.test.ts create mode 100644 api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.ts create mode 100644 api/src/paths/dwc/eml.test.ts create mode 100644 api/src/paths/dwc/eml.ts create mode 100644 api/src/paths/gcnotify/send.test.ts create mode 100644 api/src/paths/gcnotify/send.ts create mode 100644 api/src/paths/logger.test.ts create mode 100644 api/src/paths/logger.ts delete mode 100644 api/src/paths/project.test.ts delete mode 100644 api/src/paths/project.ts create mode 100644 api/src/paths/project/create.test.ts create mode 100644 api/src/paths/project/create.ts create mode 100644 api/src/paths/project/list.test.ts rename api/src/paths/{projects.ts => project/list.ts} (54%) create mode 100644 api/src/paths/project/{projectId}/attachments/report/upload.test.ts create mode 100644 api/src/paths/project/{projectId}/attachments/report/upload.ts create mode 100644 api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts create mode 100644 api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.ts create mode 100644 api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.test.ts create mode 100644 api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.ts create mode 100644 api/src/paths/project/{projectId}/participants/create.test.ts create mode 100644 api/src/paths/project/{projectId}/participants/create.ts create mode 100644 api/src/paths/project/{projectId}/participants/get.test.ts create mode 100644 api/src/paths/project/{projectId}/participants/get.ts create mode 100644 api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.test.ts create mode 100644 api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.ts create mode 100644 api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.test.ts create mode 100644 api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.test.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.test.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.test.ts create mode 100644 api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.ts delete mode 100644 api/src/paths/project/{projectId}/surveys.test.ts delete mode 100644 api/src/paths/projects.test.ts create mode 100644 api/src/paths/public/project/list.test.ts create mode 100644 api/src/paths/public/project/list.ts create mode 100644 api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts create mode 100644 api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.ts create mode 100644 api/src/paths/public/project/{projectId}/view.test.ts delete mode 100644 api/src/paths/public/projects.test.ts delete mode 100644 api/src/paths/public/projects.ts create mode 100644 api/src/paths/taxonomy/species/list.test.ts create mode 100644 api/src/paths/taxonomy/species/list.ts create mode 100644 api/src/paths/taxonomy/species/search.test.ts create mode 100644 api/src/paths/taxonomy/species/search.ts delete mode 100644 api/src/paths/user.test.ts delete mode 100644 api/src/paths/user.ts create mode 100644 api/src/paths/user/add.test.ts create mode 100644 api/src/paths/user/add.ts create mode 100644 api/src/paths/user/list.test.ts rename api/src/paths/{users.ts => user/list.ts} (52%) create mode 100644 api/src/paths/user/{userId}/delete.test.ts create mode 100644 api/src/paths/user/{userId}/delete.ts create mode 100644 api/src/paths/user/{userId}/get.test.ts create mode 100644 api/src/paths/user/{userId}/get.ts create mode 100644 api/src/paths/user/{userId}/projects/get.test.ts create mode 100644 api/src/paths/user/{userId}/projects/get.ts delete mode 100644 api/src/paths/user/{userId}/system-roles.test.ts delete mode 100644 api/src/paths/user/{userId}/system-roles.ts create mode 100644 api/src/paths/user/{userId}/system-roles/create.test.ts create mode 100644 api/src/paths/user/{userId}/system-roles/create.ts create mode 100644 api/src/paths/user/{userId}/system-roles/update.test.ts create mode 100644 api/src/paths/user/{userId}/system-roles/update.ts delete mode 100644 api/src/paths/users.test.ts create mode 100644 api/src/queries/administrative-activity/index.ts create mode 100644 api/src/queries/codes/db-constant-queries.ts create mode 100644 api/src/queries/codes/index.ts create mode 100644 api/src/queries/database/index.ts rename api/src/queries/{ => database}/user-context-queries.test.ts (91%) create mode 100644 api/src/queries/database/user-context-queries.ts create mode 100644 api/src/queries/dwc/dwc-queries.ts create mode 100644 api/src/queries/dwc/index.ts create mode 100644 api/src/queries/occurrence/index.ts create mode 100644 api/src/queries/permit/index.ts create mode 100644 api/src/queries/project-participation/index.ts create mode 100644 api/src/queries/project-participation/project-participation-queries.test.ts create mode 100644 api/src/queries/project-participation/project-participation-queries.ts rename api/src/queries/{ => project/draft}/draft-queries.test.ts (100%) rename api/src/queries/{ => project/draft}/draft-queries.ts (68%) create mode 100644 api/src/queries/project/draft/index.ts create mode 100644 api/src/queries/project/index.ts delete mode 100644 api/src/queries/project/project-view-update-queries.test.ts delete mode 100644 api/src/queries/project/project-view-update-queries.ts create mode 100644 api/src/queries/public/index.ts create mode 100644 api/src/queries/queries.ts create mode 100644 api/src/queries/search/index.ts rename api/src/queries/{ => search}/search-queries.test.ts (100%) rename api/src/queries/{ => search}/search-queries.ts (66%) create mode 100644 api/src/queries/security/index.ts rename api/src/queries/{ => spatial}/generate-geometry-collection.ts (100%) create mode 100644 api/src/queries/spatial/index.ts create mode 100644 api/src/queries/survey/index.ts delete mode 100644 api/src/queries/user-context-queries.ts create mode 100644 api/src/queries/users/index.ts create mode 100644 api/src/request-handlers/security/authentication.test.ts create mode 100644 api/src/request-handlers/security/authentication.ts create mode 100644 api/src/request-handlers/security/authorization.test.ts create mode 100644 api/src/request-handlers/security/authorization.ts delete mode 100644 api/src/security/auth-utils.test.ts delete mode 100644 api/src/security/auth-utils.ts create mode 100644 api/src/services/code-service.test.ts create mode 100644 api/src/services/code-service.ts create mode 100644 api/src/services/gcnotify-service.test.ts create mode 100644 api/src/services/gcnotify-service.ts create mode 100644 api/src/services/keycloak-service.test.ts create mode 100644 api/src/services/keycloak-service.ts create mode 100644 api/src/services/permit-service.test.ts create mode 100644 api/src/services/permit-service.ts create mode 100644 api/src/services/project-service.test.ts create mode 100644 api/src/services/project-service.ts create mode 100644 api/src/services/service.ts create mode 100644 api/src/services/survey-service.ts create mode 100644 api/src/services/taxonomy-service.test.ts create mode 100644 api/src/services/taxonomy-service.ts create mode 100644 api/src/services/user-service.test.ts create mode 100644 api/src/services/user-service.ts delete mode 100644 api/src/utils/code-utils.ts create mode 100644 api/src/utils/db-constant-utils.ts delete mode 100644 api/src/utils/path-utils.ts create mode 100644 app/src/components/attachments/EditFileWithMeta.tsx create mode 100644 app/src/components/attachments/EditReportMetaForm.tsx create mode 100644 app/src/components/attachments/FileUploadWithMeta.tsx create mode 100644 app/src/components/attachments/ReportMetaForm.tsx create mode 100644 app/src/components/boundary/FullScreenViewMapDialog.tsx create mode 100644 app/src/components/boundary/InferredLocationDetails.tsx create mode 100644 app/src/components/chips/RequestChips.tsx create mode 100644 app/src/components/dialog/EditFileWithMetaDialog.tsx create mode 100644 app/src/components/dialog/FileUploadWithMetaDialog.tsx create mode 100644 app/src/components/dialog/ViewFileWithMetaDialog.tsx create mode 100644 app/src/components/fields/SelectWithSubtext.tsx create mode 100644 app/src/components/formik/ScrollToFormikError.tsx create mode 100644 app/src/components/security/Guards.tsx create mode 100644 app/src/components/security/RouteGuards.tsx create mode 100644 app/src/components/toolbar/ActionToolbars.tsx create mode 100644 app/src/features/admin/users/AddSystemUsersForm.tsx create mode 100644 app/src/features/admin/users/UsersDetailHeader.test.tsx create mode 100644 app/src/features/admin/users/UsersDetailHeader.tsx create mode 100644 app/src/features/admin/users/UsersDetailPage.test.tsx create mode 100644 app/src/features/admin/users/UsersDetailPage.tsx create mode 100644 app/src/features/admin/users/UsersDetailProjects.test.tsx create mode 100644 app/src/features/admin/users/UsersDetailProjects.tsx create mode 100644 app/src/features/projects/participants/AddProjectParticipantsForm.tsx create mode 100644 app/src/features/projects/participants/ProjectParticipantsHeader.tsx create mode 100644 app/src/features/projects/participants/ProjectParticipantsPage.tsx create mode 100644 app/src/features/projects/view/ProjectHeader.tsx create mode 100644 app/src/features/surveys/components/PurposeAndMethodologyForm.tsx create mode 100644 app/src/features/surveys/view/SurveyHeader.tsx create mode 100644 app/src/features/surveys/view/components/SurveyPurposeAndMethodology.tsx create mode 100644 app/src/hooks/api/useN8NApi.test.ts create mode 100644 app/src/hooks/api/useTaxonomyApi.test.ts create mode 100644 app/src/hooks/api/useTaxonomyApi.ts create mode 100644 app/src/hooks/useCodes.ts create mode 100644 app/src/hooks/useInterval.test.tsx delete mode 100644 app/src/pages/access/__snapshots__/BCeIDRequestForm.test.tsx.snap create mode 100644 app/src/test-helpers/auth-helpers.ts create mode 100644 app/src/types/@tmcw/togeojson.d.ts create mode 100644 app/src/types/misc.ts delete mode 100644 app/src/utils/PrivateRoute.tsx create mode 100644 containers/metabase/Metabase.postman_collection.json create mode 100644 database/src/migrations/20211104203420_update_proprietor_type.ts create mode 100644 database/src/migrations/20211108115529_populate_summary_message_type.ts create mode 100644 database/src/migrations/20211109095230_update_system_metadata_constants.ts create mode 100644 database/src/migrations/20211109101500_update_report_attachment_description.ts create mode 100644 database/src/migrations/20211123095231_survey_status_view.ts create mode 100644 database/src/migrations/20211123095232_drop_api_get_eml_data_package.ts create mode 100644 database/src/migrations/20211126135830_update_transformation_validation_schema.ts create mode 100644 database/src/migrations/20211129180001_security_rule update.ts create mode 100644 database/src/migrations/20211201093900_insert_goat_validation_transformation.ts create mode 100644 database/src/migrations/20211202093900_insert_sheep_validation_transformation.ts create mode 100644 database/src/migrations/20211206105001_update_system_project_roles.ts create mode 100644 database/src/migrations/20211206105010_add_security_rules.ts create mode 100644 database/src/migrations/20211206121200_update_transformation_schema.ts create mode 100644 database/src/migrations/20211207105001_remove_user_roles.ts create mode 100644 database/src/migrations/20211207110001_security_token_to_occurrence.ts create mode 100644 database/src/migrations/20211207110002_update_secure_record.ts create mode 100644 database/src/migrations/20211207170011_user_group_table.ts create mode 100644 database/src/migrations/20211216095022_update_moose_template_schemas.ts create mode 100644 database/src/migrations/20220217010922_update_user_identity_source.ts create mode 100644 database/src/migrations/20220223010922_revert_user_identity_source.ts create mode 100644 database/src/migrations/20220224160300_update_moose_template_schemas.ts create mode 100644 database/src/migrations/20220228103100_update_goat_template_schemas.ts create mode 100644 database/src/migrations/20220301093900_update_sheep_validation_transformation.ts create mode 100644 database/src/migrations/20220404093900_SIMS.1.1.0.ts create mode 100644 database/src/migrations/20220419152015_add_surveyed_areas_column.ts create mode 100644 database/src/migrations/20220428150001_update_api_delete_survey.ts rename database/src/migrations/{release.0.34/smoketest_release.sql => smoke_tests/smoketest_release.1.0.0.sql} (99%) create mode 100644 database/src/migrations/smoke_tests/smoketest_release.1.1.0.sql create mode 100644 database/src/migrations/template_methodology_species_transformations/goat_composition_or_recruitment_1.json create mode 100644 database/src/migrations/template_methodology_species_transformations/goat_composition_or_recruitment_2.json create mode 100644 database/src/migrations/template_methodology_species_transformations/moose_srb_or_composition_survey_3.json create mode 100644 database/src/migrations/template_methodology_species_transformations/moose_srb_or_composition_survey_4.json create mode 100644 database/src/migrations/template_methodology_species_transformations/moose_srb_or_composition_survey_5.json create mode 100644 database/src/migrations/template_methodology_species_transformations/moose_srb_or_composition_survey_6.json create mode 100644 database/src/migrations/template_methodology_species_transformations/sheep_composition_or_recruitment_1.json create mode 100644 database/src/migrations/template_methodology_species_transformations/sheep_composition_or_recruitment_2.json create mode 100644 database/src/migrations/template_methodology_species_validations/goat_composition_or_recruitment_1.json create mode 100644 database/src/migrations/template_methodology_species_validations/goat_composition_or_recruitment_2.ts create mode 100644 database/src/migrations/template_methodology_species_validations/moose_srb_or_composition_survey_4.json create mode 100644 database/src/migrations/template_methodology_species_validations/moose_srb_or_composition_survey_5.json create mode 100644 database/src/migrations/template_methodology_species_validations/moose_srb_or_composition_survey_6.ts create mode 100644 database/src/migrations/template_methodology_species_validations/picklist_variables/v0.1.ts create mode 100644 database/src/migrations/template_methodology_species_validations/sheep_composition_or_recruitment_1.json create mode 100644 database/src/migrations/template_methodology_species_validations/sheep_composition_or_recruitment_2.json create mode 100644 database/src/migrations/template_methodology_species_validations/sheep_composition_or_recruitment_3.ts create mode 100644 testing/e2e/cypress/fixtures/1.doc create mode 100644 testing/e2e/cypress/fixtures/1.xlsx delete mode 100644 testing/e2e/cypress/support/createUUID.ts create mode 100644 testing/e2e/cypress/support/keycloak.js create mode 100644 testing/zap/README.md create mode 100644 testing/zap/dev.context diff --git a/.config/config.json b/.config/config.json index ad7ed42db7..6ace64a88b 100644 --- a/.config/config.json +++ b/.config/config.json @@ -43,8 +43,8 @@ "prod": "https://oidc.gov.bc.ca/auth/realms/35r1iman/protocol/openid-connect/certs" }, "siteminderLogoutURL": { - "dev": "https://logontest.gov.bc.ca/clp-cgi/logoff.cgi", - "test": "https://logontest.gov.bc.ca/clp-cgi/logoff.cgi", + "dev": "https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi", + "test": "https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi", "prod": "https://logon7.gov.bc.ca/clp-cgi/logoff.cgi" }, "sso": { diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fc9e8e019b..2f9d6b7255 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,31 +1,19 @@ # Overview -## Links to jira tickets +## Links to Jira tickets -- {Include links to all of the applicable Jira tickets} +- {List all applicable Jira tickets} -## This PR contains the following changes +## Description of relevant changes -- {List all the relevant changes. If there are many changes across many Jira tickets, organize the changes by Jira ticket} +- {List all relevant changes, in particular anything that will help the reviewers test/verify this PR} -## This PR contains the following types of changes - -- [ ] New feature (change which adds functionality) -- [ ] Enhancement (improvements to existing functionality) -- [ ] Bug fix (change which fixes an issue) -- [ ] Misc cleanup / Refactoring / Documentation -- [ ] Version change - -## Checklist +## PR Checklist A list of items that are good to consider when making any changes. _Note: this list is not exhaustive, and not all items are always applicable._ -### General - -- [ ] I have performed a self-review of my own code - ### Code - [ ] New files/classes/functions have appropriately descriptive names and comment blocks to describe their use/behaviour @@ -60,11 +48,3 @@ _Note: this list is not exhaustive, and not all items are always applicable._ ### SonarCloud - [ ] I have addressed all SonarCloud Bugs, Vulnerabilities, Security Hotspots, and Code Smells - -## How Has This Been Tested? - -Please describe the tests that you ran to verify your changes. - -## Screenshots - -Please add any relevant UI screenshots, if applicable. diff --git a/.github/workflows/addComments.yml b/.github/workflows/addComments.yml new file mode 100644 index 0000000000..81d26f32bb --- /dev/null +++ b/.github/workflows/addComments.yml @@ -0,0 +1,23 @@ +# Add automated comments to the PR +name: Add Comments + +on: + pull_request: + types: [opened] + branches: + - dev + +jobs: + addOpenshiftURLComment: + name: Add Openshift URL Comment + runs-on: ubuntu-latest + steps: + - name: Add Comment + uses: peter-evans/create-or-update-comment@v2 + with: + issue-number: ${{ github.event.number }} + body: | + Openshift URLs for the PR Deployment: + - App Route: https://biohubbc-app-${{ github.event.number }}-af2668-dev.apps.silver.devops.gov.bc.ca/ + - Api Route: https://biohubbc-api-${{ github.event.number }}-af2668-dev.apps.silver.devops.gov.bc.ca/ + - Pods: https://console.apps.silver.devops.gov.bc.ca/k8s/ns/af2668-dev/pods?name=${{ github.event.number }} diff --git a/.github/workflows/cleanClosedPR.yml b/.github/workflows/cleanClosedPR.yml index 186e41771d..c393772b86 100644 --- a/.github/workflows/cleanClosedPR.yml +++ b/.github/workflows/cleanClosedPR.yml @@ -22,9 +22,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6238b10d81..c28d4958a1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -59,9 +59,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -107,9 +107,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -155,9 +155,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -203,9 +203,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -252,9 +252,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -301,9 +301,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -351,9 +351,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -400,9 +400,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -440,7 +440,11 @@ jobs: CYPRESS_username: ${{ secrets.CYPRESS_USER_NAME }} CYPRESS_password: ${{ secrets.CYPRESS_PASSWORD }} CYPRESS_BASE_URL: 'https://biohubbc-app-${{ github.event.number }}-af2668-dev.apps.silver.devops.gov.bc.ca' + CYPRESS_host: 'https://biohubbc-app-${{ github.event.number }}-af2668-dev.apps.silver.devops.gov.bc.ca' CYPRESS_ENVIRONMENT: ${{ github.base_ref }} + CYPRESS_authRealm: '35r1iman' + CYPRESS_authClientId: 'biohubbc' + CYPRESS_authUrl: 'https://${{ github.base_ref }}.oidc.gov.bc.ca' needs: - deployDatabase - deployDatabaseSetup @@ -488,4 +492,7 @@ jobs: echo Git Change ID: ${{ github.event.number }} echo Cypress BaseUrl: $CYPRESS_BASE_URL echo Cypress Host: $CYPRESS_ENVIRONMENT + echo $CYPRESS_authRealm + echo $CYPRESS_authClientId + echo $CYPRESS_authUrl diff --git a/.github/workflows/deployStatic.yml b/.github/workflows/deployStatic.yml index bf74f5b61b..bb8cf9ac10 100644 --- a/.github/workflows/deployStatic.yml +++ b/.github/workflows/deployStatic.yml @@ -64,9 +64,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -112,9 +112,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -161,9 +161,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -208,9 +208,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -257,9 +257,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -307,9 +307,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -357,9 +357,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -406,9 +406,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -457,9 +457,9 @@ jobs: # Install Node - for `node` and `npm` commands - name: Setup Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: 10.16 + node-version: 14 # Cache Node modules - name: Cache node modules @@ -516,6 +516,28 @@ jobs: oc project af2668-tools oc get all,pvc,secret,pods,ReplicationController,DeploymentConfig,HorizontalPodAutoscaler,imagestreamtag -o name | grep biohubbc | grep $BUILD_ID | awk '{print "oc delete " $1}' | bash + cycleschemaspy: + name: Cycle SchemaSpy to refresh after database update in dev + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.merged == true && github.event.pull_request.draft == false && github.base_ref == 'dev' }} + env: + BUILD_ID: ${{ github.event.number }} + needs: + - checkEnv + - deployDatabaseSetup + steps: + # Log in to OpenShift. + # Note: The secrets needed to log in are NOT available if the PR comes from a FORK. + # PR's must originate from a branch off the original repo or else all openshift `oc` commands will fail. + - name: Log in to OpenShift + run: oc login --token=${{ secrets.TOOLS_SA_TOKEN }} --server=https://api.silver.devops.gov.bc.ca:6443 + + - name: Scale down + run: | + oc project af2668-dev + oc scale --replicas=0 dc schemaspy + oc scale --replicas=1 dc schemaspy + cypress-run: runs-on: ubuntu-latest if: ${{ github.event.pull_request.merged == true && github.event.pull_request.draft == false && github.base_ref != 'prod' }} @@ -523,8 +545,12 @@ jobs: CYPRESS_RECORD_KEY: ${{ secrets.RECORDING_KEY }} CYPRESS_username: ${{ secrets.CYPRESS_USER_NAME }} CYPRESS_password: ${{ secrets.CYPRESS_PASSWORD }} - CYPRESS_BASE_URL: 'https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca/' + CYPRESS_BASE_URL: "https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca" + CYPRESS_host: "https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca" CYPRESS_ENVIRONMENT: ${{ github.base_ref }} + CYPRESS_authRealm: "35r1iman" + CYPRESS_authClientId: "biohubbc" + CYPRESS_authUrl: "https://${{ github.base_ref }}.oidc.gov.bc.ca" needs: - deployDatabase - deployDatabaseSetup @@ -538,7 +564,7 @@ jobs: - name: Wait for API response uses: nev7n/wait_for_response@v1.0.1 with: - url: 'https://api-${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca/version' + url: "https://api-${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca/version" responseCode: 200 timeout: 240000 interval: 500 @@ -546,9 +572,9 @@ jobs: - name: Wait for APP response uses: nev7n/wait_for_response@v1.0.1 with: - url: 'https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca/' + url: "https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca" responseCode: 200 - timeout: 120000 + timeout: 240000 interval: 500 - name: E2E Smoke tests @@ -558,8 +584,8 @@ jobs: id: smoke continue-on-error: false with: - wait-on: 'https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca/' - wait-on-timeout: 120 + wait-on: "https://${{ github.base_ref }}-biohubbc.apps.silver.devops.gov.bc.ca" + wait-on-timeout: 240 record: true working-directory: testing/e2e @@ -567,9 +593,11 @@ jobs: run: | echo Git Base Ref: ${{ github.base_ref }} echo Git Change ID: ${{ github.event.number }} - echo Cypress Record Key: $CYPRESS_RECORD_KEY echo Cypress BaseUrl: $CYPRESS_BASE_URL echo Cypress Host: $CYPRESS_ENVIRONMENT + echo $CYPRESS_authRealm + echo $CYPRESS_authClientId + echo $CYPRESS_authUrl notify: name: Discord Notification diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index d4d4721814..3102a53d2c 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -10,12 +10,12 @@ jobs: name: Running Formatter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: "10" + node-version: 14 - name: Cache database node modules uses: actions/cache@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c40910628..70066548bd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,12 +10,12 @@ jobs: name: Running Linter runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: "10" + node-version: 14 - name: Cache database node modules uses: actions/cache@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c81ba20c9..cf36386051 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,12 +13,12 @@ jobs: name: Running Tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Setup node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: - node-version: "12" + node-version: 14 - name: Cache api node modules uses: actions/cache@v2.1.6 diff --git a/.github/workflows/zap.yml b/.github/workflows/zap.yml new file mode 100644 index 0000000000..917aa7b73e --- /dev/null +++ b/.github/workflows/zap.yml @@ -0,0 +1,27 @@ +name: zap +on: workflow_dispatch + +jobs: + zap_scan: + runs-on: ubuntu-latest + name: Scan the webapplication + env: + CYPRESS_password: ${{ secrets.CYPRESS_PASSWORD }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: dev + - name: Subtitute Password + working-directory: ./testing/zap + run: | + echo Path: $(pwd) + LOCAL_VAR=$(echo 'cypressidir$CYPRESS_PW' | base64) + sed -i 's/@pw@/'$LOCAL_VAR'/g' dev.context + - name: ZAP Scan + uses: zaproxy/action-full-scan@v0.3.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} + docker_name: 'owasp/zap2docker-stable' + target: 'https://dev-biohubbc.apps.silver.devops.gov.bc.ca/' + cmd_options: '-a' diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 654df2ac33..34a7b4de53 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -5,8 +5,8 @@ sonar.projectName=BioHubBC sonar.projectVersion=Autoscan # Path to sources -sonar.sources=app/src,api/src,database -sonar.exclusions=**/*.test.*,**/*.spec.*,**/__tests__/**,**/__snapshots__/**,**/constants/**,**/shared-api-docs.ts +sonar.sources=app/src,api/src +sonar.exclusions=**/*.test.*,**/*.spec.*,**/__tests__/**,**/__snapshots__/**,**/__mocks__/**,**/constants/**,**/shared-api-docs.ts #sonar.inclusions= # Path to tests diff --git a/Makefile b/Makefile index 64f59cd02c..06a61125c8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ #!make # ------------------------------------------------------------------------------ -# Makefile -- BioHubBC +# Makefile -- SIMS # ------------------------------------------------------------------------------ -include .env @@ -10,7 +10,7 @@ export $(shell sed 's/=.*//' .env) .DEFAULT : help -.PHONY : setup close clean build run run-debug build-backend run-backend run-backend-debug build-web run-web run-web-debug database app api db-setup db-migrate db-rollback n8n-setup n8n-export clamav install test lint lint-fix format help +.PHONY : setup close clean build-backend run-backend build-web run-web database app api db-setup db-migrate db-rollback n8n-setup n8n-export clamav install test cypress lint lint-fix format format-fix help ## ------------------------------------------------------------------------------ ## Alias Commands @@ -19,30 +19,20 @@ export $(shell sed 's/=.*//' .env) # Running the docker build # 1. Run `make env` -# 2. Edit the `.env` file as needed to update variables and secrets -# 3. Run `make local` or `make local-debug` +# 2. Edit the `.env` file as needed to update variables and secrets +# 3. Run `make web` env: | setup ## Copies the default ./env_config/env.docker to ./.env -all: | close build run ## Performs all commands necessary to run all projects in docker -all-debug: | close build run-debug ## Performs all commands necessary to run all projects in docker in debug mode - -postgres: | close build-postgres run-postgres ## Performs all commands necessary to run the postgres db project in docker -postgres-debug: | close build-postgres run-postgres-debug ## Performs all commands necessary to run the postgres db project in docker in debug mode - -backend: | close build-backend run-backend ## Performs all commands necessary to run all backend projects in docker -backend-debug: | close build-backend run-backend-debug ## Performs all commands necessary to run all backend projects in docker in debug mode - -web: | close build-web run-web ## Performs all commands necessary to run all backend+web projects in docker -web-debug: | close build-web run-web-debug ## Performs all commands necessary to run all backend+web projects in docker in debug mode +postgres: | close build-postgres run-postgres ## Performs all commands necessary to run the postgres project (db) in docker +backend: | close build-backend run-backend ## Performs all commands necessary to run all backend projects (db, api) in docker +web: | close build-web run-web ## Performs all commands necessary to run all backend+web projects (db, api, app, n8n) in docker db-setup: | build-db-setup run-db-setup ## Performs all commands necessary to run the database migrations and seeding db-migrate: | build-db-migrate run-db-migrate ## Performs all commands necessary to run the database migrations db-rollback: | build-db-rollback run-db-rollback ## Performs all commands necessary to rollback the latest database migrations - n8n-setup: | build-n8n-setup run-n8n-setup ## Performs all commands necessary to run the n8n setup n8n-export: | build-n8n-export run-n8n-export ## Performs all commands necessary to export the latest n8n credentials and workflows - clamav: | build-clamav run-clamav ## Performs all commands necessary to run clamav ## ------------------------------------------------------------------------------ @@ -67,32 +57,9 @@ clean: ## Closes and cleans (removes) all project containers @echo "===============================================" @docker-compose -f docker-compose.yml down -v --rmi all --remove-orphans -## ------------------------------------------------------------------------------ -## Build/Run Backend+Frontend Commands -## - Builds all of the biohub projects (db, db_setup, api, app) -## ------------------------------------------------------------------------------ - -build: ## Builds all project containers - @echo "===============================================" - @echo "Make: build - building all project images" - @echo "===============================================" - @docker-compose -f docker-compose.yml build - -run: ## Runs all project containers - @echo "===============================================" - @echo "Make: run - running all project images" - @echo "===============================================" - @docker-compose -f docker-compose.yml up -d - -run-debug: ## Runs all project containers in debug mode, where all container output is printed to the console - @echo "===============================================" - @echo "Make: run-debug - running all project images in debug mode" - @echo "===============================================" - @docker-compose -f docker-compose.yml up - ## ------------------------------------------------------------------------------ ## Build/Run Postgres DB Commands -## - Builds all of the biohub postgres db projects (db, db_setup) +## - Builds all of the SIMS postgres db projects (db, db_setup) ## ------------------------------------------------------------------------------ build-postgres: ## Builds the postgres db containers @@ -107,15 +74,9 @@ run-postgres: ## Runs the postgres db containers @echo "===============================================" @docker-compose -f docker-compose.yml up -d db db_setup -run-postgres-debug: ## Runs the postgres db containers in debug mode, where all container output is printed to the console - @echo "===============================================" - @echo "Make: run-postgres-debug - running postgres db images in debug mode" - @echo "===============================================" - @docker-compose -f docker-compose.yml up db db_setup - ## ------------------------------------------------------------------------------ ## Build/Run Backend Commands -## - Builds all of the biohub backend projects (db, db_setup, api) +## - Builds all of the SIMS backend projects (db, db_setup, api) ## ------------------------------------------------------------------------------ build-backend: ## Builds all backend containers @@ -130,15 +91,9 @@ run-backend: ## Runs all backend containers @echo "===============================================" @docker-compose -f docker-compose.yml up -d db db_setup api -run-backend-debug: ## Runs all backend containers in debug mode, where all container output is printed to the console - @echo "===============================================" - @echo "Make: run-backend-debug - running backend images in debug mode" - @echo "===============================================" - @docker-compose -f docker-compose.yml up db db_setup api - ## ------------------------------------------------------------------------------ ## Build/Run Backend+Web Commands (backend + web frontend) -## - Builds all of the biohub backend+web projects (db, db_setup, api, app, n8n, n8n_nginx, n8n_setup) +## - Builds all of the SIMS backend+web projects (db, db_setup, api, app, n8n, n8n_nginx, n8n_setup) ## ------------------------------------------------------------------------------ build-web: ## Builds all backend+web containers @@ -155,12 +110,6 @@ run-web: ## Runs all backend+web containers ## Restart n8n as a workaround to resolve this known issue: https://github.com/n8n-io/n8n/issues/2155 @docker-compose restart n8n -run-web-debug: ## Runs all backend+web containers in debug mode, where all container output is printed to the console - @echo "===============================================" - @echo "Make: run-web-debug - running web images in debug mode" - @echo "===============================================" - @docker-compose -f docker-compose.yml up db db_setup api app n8n n8n_nginx n8n_setup - ## ------------------------------------------------------------------------------ ## Commands to shell into the target container ## ------------------------------------------------------------------------------ @@ -416,4 +365,4 @@ log-n8n-nginx: ## Runs `docker logs -f` for the n8n nginx container ## ------------------------------------------------------------------------------ help: ## Display this help screen. - @grep -h -E '^[a-zA-Z_-]+:.*?##.*$$|^##.*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-20s\033[0m %s\n", $$1, $$2}' | awk 'BEGIN {FS = "## "}; {printf "\033[36m%-1s\033[0m %s\n", $$2, $$1}' + @grep -h -E '^[0-9a-zA-Z_-]+:.*?##.*$$|^##.*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-20s\033[0m %s\n", $$1, $$2}' | awk 'BEGIN {FS = "## "}; {printf "\033[36m%-1s\033[0m %s\n", $$2, $$1}' diff --git a/README.md b/README.md index bfedd28e1e..73f3814f6f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Sub-project under the SEISM Capital project, the source of BC’s species inventory data. -The objectives for the BioHubBC project are: +The objectives for the SIMS project are: - To provide a single source for aquatic and terrestrial species and habitat data. - To reduce the barriers for collecting and sharing aquatic and terrestrial species and habitat data throughout the province of British Columbia. @@ -32,12 +32,14 @@ The objectives for the BioHubBC project are: ### Windows +_Note: there are 2 mutually exclusive modes that Docker Desktop supports on Windows: Hyper-V or WSL2. You should be able to run the application in either mode, but this documentation was only written with instructions for Hyper-V. See https://code.visualstudio.com/blogs/2020/03/02/docker-in-wsl2 for possible instructions on using Docker Desktop in WSL2._ + If prompted, install Docker using Hyper-V (not WSL 2) ### Grant Docker access to your local folders -This setup for biohub uses volumes to support live reload. -To leverage live reload you will need to ensure Docker is running using Hyper-V (not the WSL2 engine). +This setup uses volumes to support live reload. +Ensure Docker Desktop has access to your file system so that it can detect file changes and trigger live reload. #### MacOS @@ -113,9 +115,9 @@ make env Result of running `make env` for the first time: ![make env screenshot](readme_screenshots/running_make_env.png "Running `make env`") -## Start all BioHub Applications +## Start all Applications -Starts all applications (database, api, and web app). +Starts all applications (database, api, app, and n8n). ``` make web @@ -134,6 +136,10 @@ app: - `localhost:7100` +n8n: + +- `localhost:5100` + # Helpful Makefile Commands See `./Makefile` for all available commands. @@ -156,7 +162,7 @@ make install ## Delete All Containers -Will stop and delete the biohub docker containers. +Will stop and delete the application docker containers. This is useful when you want to clear out all database content, returning it to its initial default state. After you've run `make clean`, running `make web` will launch new containers, with a fresh instance of the database. @@ -322,7 +328,6 @@ _Note: all of the above connection values can be found in the `.env` file_ [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-black.svg)](https://sonarcloud.io/dashboard?id=bcgov_biohubbc) - # License Copyright 2019 Province of British Columbia diff --git a/api/.docker/api/Dockerfile b/api/.docker/api/Dockerfile index 91ba49ff97..8c7457949e 100644 --- a/api/.docker/api/Dockerfile +++ b/api/.docker/api/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10 +FROM node:14 ENV HOME=/opt/app-root/src diff --git a/api/.eslintrc b/api/.eslintrc index 83f6255db9..db41111d79 100644 --- a/api/.eslintrc +++ b/api/.eslintrc @@ -25,6 +25,15 @@ "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/ban-types": ["error", { "types": { "object": false, "extendDefaults": true } }], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": false, + "ts-ignore": false, + "ts-nocheck": false, + "ts-check": false + } + ], "no-var": "error" } } diff --git a/api/.mocharc.json b/api/.mocharc.json index c3186d934f..4fe42af1de 100644 --- a/api/.mocharc.json +++ b/api/.mocharc.json @@ -1,5 +1,5 @@ { "extension": ["ts"], "spec": "src/{*.@(test|spec).ts,!(__mocks__)/**/*.@(test|spec).ts}", - "require": "ts-node/register" + "require": "ts-node/register, mocha-fixtures.ts" } diff --git a/api/.pipeline/config.js b/api/.pipeline/config.js index 9f8b9f25bf..06e1c0ed76 100644 --- a/api/.pipeline/config.js +++ b/api/.pipeline/config.js @@ -5,7 +5,9 @@ let options = require('pipeline-cli').Util.parseArguments(); const config = require('../../.config/config.json'); const defaultHost = 'biohubbc-af2668-api.apps.silver.devops.gov.bc.ca'; +const defaultHostAPP = 'biohubbc-af2668-dev.apps.silver.devops.gov.bc.ca'; +const appName = (config.module && config.module['app']) || 'biohubbc-app'; const name = (config.module && config.module['api']) || 'biohubbc-api'; const dbName = (config.module && config.module['db']) || 'biohubbc-db'; @@ -21,6 +23,7 @@ const tag = (branch && `build-${version}-${changeId}-${branch}`) || `build-${ver const staticBranches = config.staticBranches || []; const staticUrlsAPI = config.staticUrlsAPI || {}; +const staticUrls = config.staticUrls || {}; const processOptions = (options) => { const result = { ...options }; @@ -58,9 +61,10 @@ const phases = { version: `${version}-${changeId}`, tag: tag, env: 'build', + elasticsearchURL: 'https://elasticsearch-af2668-dev.apps.silver.devops.gov.bc.ca', tz: config.timezone.api, branch: branch, - logLevel: 'debug' + logLevel: isStaticDeployment && 'info' || 'debug' }, dev: { namespace: 'af2668-dev', @@ -75,12 +79,16 @@ const phases = { host: (isStaticDeployment && (staticUrlsAPI.dev || defaultHost)) || `${name}-${changeId}-af2668-dev.apps.silver.devops.gov.bc.ca`, + appHost: + (isStaticDeployment && (staticUrls.dev || defaultHostAPP)) || + `${appName}-${changeId}-af2668-dev.apps.silver.devops.gov.bc.ca`, env: 'dev', + elasticsearchURL: 'https://elasticsearch-af2668-dev.apps.silver.devops.gov.bc.ca', tz: config.timezone.api, certificateURL: config.certificateURL.dev, replicas: 1, maxReplicas: 2, - logLevel: 'debug' + logLevel: isStaticDeployment && 'info' || 'debug' }, test: { namespace: 'af2668-test', @@ -94,11 +102,12 @@ const phases = { tag: `test-${version}`, host: staticUrlsAPI.test, env: 'test', + elasticsearchURL: 'https://elasticsearch-af2668-dev.apps.silver.devops.gov.bc.ca', tz: config.timezone.api, certificateURL: config.certificateURL.test, replicas: 3, maxReplicas: 5, - logLevel: 'debug' + logLevel: 'info' }, prod: { namespace: 'af2668-prod', @@ -112,6 +121,7 @@ const phases = { tag: `prod-${version}`, host: staticUrlsAPI.prod, env: 'prod', + elasticsearchURL: 'http://es01:9200', tz: config.timezone.api, certificateURL: config.certificateURL.prod, replicas: 3, diff --git a/api/.pipeline/lib/api.deploy.js b/api/.pipeline/lib/api.deploy.js index 2ad33e1e85..b30076da6b 100644 --- a/api/.pipeline/lib/api.deploy.js +++ b/api/.pipeline/lib/api.deploy.js @@ -29,7 +29,9 @@ module.exports = (settings) => { VERSION: phases[phase].tag, HOST: phases[phase].host, CHANGE_ID: phases.build.changeId || changeId, + APP_HOST: phases[phase].appHost, NODE_ENV: phases[phase].env || 'dev', + ELASTICSEARCH_URL: phases[phase].elasticsearchURL, TZ: phases[phase].tz, DB_SERVICE_NAME: `${phases[phase].dbName}-postgresql${phases[phase].suffix}`, CERTIFICATE_URL: phases[phase].certificateURL, diff --git a/api/.pipeline/package.json b/api/.pipeline/package.json index 69fbe13d52..71e7493dcd 100644 --- a/api/.pipeline/package.json +++ b/api/.pipeline/package.json @@ -4,7 +4,8 @@ "description": "Contains dependencies and scripts for executing OpenShift pipeline build/deploy scripts", "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": ">= 14.0.0", + "npm": ">= 6.0.0" }, "repository": { "type": "git", diff --git a/api/Dockerfile b/api/Dockerfile index a4e7514acf..622cc282ec 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,7 +2,7 @@ # This DockerFile is used for deployments only, please do not change for other purposes. It will break our deployment. # #################################################################################################################### -FROM node:10 +FROM node:14 ENV HOME=/opt/app-root/src diff --git a/api/README.md b/api/README.md index e0a7c303ce..ee143762c1 100644 --- a/api/README.md +++ b/api/README.md @@ -4,9 +4,8 @@ | Technology | Version | Website | Description | | ---------- | ------- | ------------------------------------ | -------------------- | -| node | 10.x.x | https://nodejs.org/en/ | JavaScript Runtime | +| node | 14.x.x | https://nodejs.org/en/ | JavaScript Runtime | | npm | 6.x.x | https://www.npmjs.com/ | Node Package Manager | -| PostgreSQL | 9.6 | https://www.postgresql.org/download/ | PSQL database |
@@ -14,7 +13,9 @@ The root API schema is defined in `./src/openapi/api.ts`. -If this project is running in docker you can view the api docs at: `http://localhost:6100/api/api-docs/`. +If this project is running in docker you can view the beautified api docs at: `http://localhost:6100/api-docs/`. + +- The raw api-docs are available at: `http://localhost:6100/raw-api-docs/`. This project uses npm package `express-openapi` via `./app.ts` to automatically generate the express server and its routes, based on the contents of the `./src/openapi/api.ts` and the `./src/path/` content. diff --git a/api/mocha-fixtures.ts b/api/mocha-fixtures.ts new file mode 100644 index 0000000000..27d3638ea8 --- /dev/null +++ b/api/mocha-fixtures.ts @@ -0,0 +1,8 @@ +import { setLogLevel } from './src/utils/logger'; + +// See https://mochajs.org/#global-setup-fixtures +exports.mochaGlobalSetup = async function () { + // Disable winston logging before mocha unit tests run, to prevent winston from cluttering the test log with test + // error messages. + setLogLevel('silent'); +}; diff --git a/api/openshift/api.bc.yaml b/api/openshift/api.bc.yaml index 3a4c9b2531..e9e161c74b 100644 --- a/api/openshift/api.bc.yaml +++ b/api/openshift/api.bc.yaml @@ -30,13 +30,13 @@ parameters: value: dev - name: BASE_IMAGE_URL required: true - value: registry.access.redhat.com/ubi8/nodejs-10:1-41 + value: image-registry.openshift-image-registry.svc:5000/openshift/nodejs:14-ubi8 - name: SOURCE_IMAGE_NAME required: true - value: redhat-ubi-node-10 + value: nodejs - name: SOURCE_IMAGE_TAG required: true - value: 1-41 + value: 14-ubi8 objects: - kind: ImageStream apiVersion: v1 @@ -91,8 +91,8 @@ objects: cpu: 1250m memory: 3Gi requests: - cpu: 750m - memory: 2Gi + cpu: 100m + memory: 512Mi runPolicy: SerialLatestOnly source: contextDir: '${SOURCE_CONTEXT_DIR}' diff --git a/api/openshift/api.dc.yaml b/api/openshift/api.dc.yaml index 85a3d58b22..9c3a1ada84 100644 --- a/api/openshift/api.dc.yaml +++ b/api/openshift/api.dc.yaml @@ -17,6 +17,9 @@ parameters: - name: HOST description: Host name of the application required: true + - name: APP_HOST + description: APP host for application frontend + value: '' - name: CHANGE_ID description: Change id of the project. This will help to pull image stream required: true @@ -28,6 +31,10 @@ parameters: description: Application Environment type variable required: true value: 'dev' + - name: ELASTICSEARCH_URL + description: Platform Elasticsearch URL + required: true + value: 'https://elasticsearch-af2668-dev.apps.silver.devops.gov.bc.ca' - name: TZ description: Application timezone required: false @@ -36,16 +43,18 @@ parameters: description: Authentication certificate urls required: true value: 'https://oidc.gov.bc.ca/auth/realms/35r1iman/protocol/openid-connect/certs' - - name: CPU_REQ - value: '500m' - - name: CPU_LIMIT - value: '750m' - - name: MEMORY_REQ - value: '1Gi' - - name: MEMORY_LIMIT - value: '2Gi' - - name: REPLICAS - value: '1' + - name: KEYCLOAK_HOST + description: keycloak host url + value: https://dev.oidc.gov.bc.ca + - name: KEYCLOAK_REALM + description: keycloak realm definition + value: 35r1iman + - name: KEYCLOAK_ADMIN_USERNAME + description: keycloak host admin username + value: biohubbc-svc + - name: KEYCLOAK_ADMIN_PASSWORD + description: keycloak host url + value: 'keycloak-admin-password' - name: API_PORT_DEFAULT value: '6100' - name: API_PORT_DEFAULT_NAME @@ -54,11 +63,37 @@ parameters: - name: OBJECT_STORE_SECRETS description: Secrets used to read and write to the S3 storage value: 'biohubbc-object-store' + - name: LOG_LEVEL + value: 'info' + - name: GCNOTIFY_API_SECRET + description: Secret for gcnotify api key + value: 'gcnotify-api-key' + - name: GCNOTIFY_ADMIN_EMAIL + description: admin email for gcnotify api + value: biohub@gov.bc.ca + - name: GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE + description: gcnotify email template id + value: 7779a104-b863-40ac-902f-1aa607d2071a + - name: GCNOTIFY_ONBOARDING_REQUEST_SMS_TEMPLATE + description: gcnotify sms template id + value: af2f1e40-bd72-4612-9c5a-567ee5b26ca5 + - name: GCNOTIFY_EMAIL_URL + value: https://api.notification.canada.ca/v2/notifications/email + - name: GCNOTIFY_SMS_URL + value: https://api.notification.canada.ca/v2/notifications/sms + - name: CPU_REQUEST + value: '100m' + - name: CPU_LIMIT + value: '500m' + - name: MEMORY_REQUEST + value: '512Mi' + - name: MEMORY_LIMIT + value: '2Gi' + - name: REPLICAS + value: '1' - name: REPLICA_MAX required: true value: '1' - - name: LOG_LEVEL - value: 'info' objects: - apiVersion: image.openshift.io/v1 kind: ImageStream @@ -99,8 +134,8 @@ objects: cpu: ${CPU_LIMIT} memory: ${MEMORY_LIMIT} requests: - cpu: ${CPU_REQ} - memory: ${MEMORY_REQ} + cpu: ${CPU_REQUEST} + memory: ${MEMORY_REQUEST} type: Rolling template: metadata: @@ -116,6 +151,8 @@ objects: value: ${HOST} - name: API_PORT value: ${API_PORT_DEFAULT} + - name: APP_HOST + value: ${APP_HOST} - name: ENABLE_FILE_VIRUS_SCAN value: ${ENABLE_FILE_VIRUS_SCAN} - name: DB_HOST @@ -139,10 +176,23 @@ objects: value: '5432' - name: KEYCLOAK_URL value: ${CERTIFICATE_URL} + - name: KEYCLOAK_HOST + value: ${KEYCLOAK_HOST} + - name: KEYCLOAK_REALM + value: ${KEYCLOAK_REALM} + - name: KEYCLOAK_ADMIN_USERNAME + value: ${KEYCLOAK_ADMIN_USERNAME} + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + key: keycloak_admin_password + name: ${KEYCLOAK_ADMIN_PASSWORD} - name: CHANGE_VERSION value: ${CHANGE_ID} - name: NODE_ENV value: ${NODE_ENV} + - name: ELASTICSEARCH_URL + value: ${ELASTICSEARCH_URL} - name: TZ value: ${TZ} - name: VERSION @@ -169,6 +219,21 @@ objects: name: ${OBJECT_STORE_SECRETS} - name: LOG_LEVEL value: ${LOG_LEVEL} + - name: GCNOTIFY_SECRET_API_KEY + valueFrom: + secretKeyRef: + key: key + name: ${GCNOTIFY_API_SECRET} + - name: GCNOTIFY_ADMIN_EMAIL + value: ${GCNOTIFY_ADMIN_EMAIL} + - name: GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE + value: ${GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE} + - name: GCNOTIFY_ONBOARDING_REQUEST_SMS_TEMPLATE + value: ${GCNOTIFY_ONBOARDING_REQUEST_SMS_TEMPLATE} + - name: GCNOTIFY_EMAIL_URL + value: ${GCNOTIFY_EMAIL_URL} + - name: GCNOTIFY_SMS_URL + value: ${GCNOTIFY_SMS_URL} image: ' ' imagePullPolicy: Always name: api @@ -180,8 +245,8 @@ objects: cpu: ${CPU_LIMIT} memory: ${MEMORY_LIMIT} requests: - cpu: ${CPU_REQ} - memory: ${MEMORY_REQ} + cpu: ${CPU_REQUEST} + memory: ${MEMORY_REQUEST} readinessProbe: failureThreshold: 10 httpGet: diff --git a/api/package-lock.json b/api/package-lock.json index f51541e702..5537c2d33e 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -1,8 +1,12427 @@ { - "name": "biohubbc-api", + "name": "sims-api", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "sims-api", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@elastic/elasticsearch": "~8.1.0", + "adm-zip": "~0.5.5", + "ajv": "~8.6.3", + "aws-sdk": "~2.742.0", + "axios": "~0.21.4", + "clamdjs": "~1.0.2", + "db-migrate": "~0.11.11", + "db-migrate-pg": "~1.2.2", + "express": "~4.17.1", + "express-openapi": "~9.3.0", + "jsonpath": "~1.1.1", + "jsonwebtoken": "~8.5.1", + "jwks-rsa": "~2.0.5", + "knex": "~1.0.1", + "lodash": "~4.17.21", + "mime": "~2.5.2", + "moment": "~2.29.2", + "multer": "~1.4.3", + "pg": "~8.7.1", + "qs": "~6.10.1", + "sql-template-strings": "~2.2.2", + "swagger-ui-express": "~4.3.0", + "typescript": "~4.1.6", + "uuid": "~8.3.2", + "winston": "~3.3.3", + "xlsx": "~0.17.0", + "xml2js": "~0.4.23" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "~1.0.1", + "@types/adm-zip": "~0.4.34", + "@types/chai": "~4.2.22", + "@types/express": "~4.17.13", + "@types/geojson": "~7946.0.8", + "@types/gulp": "~4.0.9", + "@types/jsonpath": "~0.2.0", + "@types/jsonwebtoken": "~8.5.5", + "@types/lodash": "~4.14.176", + "@types/mime": "~2.0.3", + "@types/mocha": "~9.0.0", + "@types/multer": "~1.4.7", + "@types/node": "~14.14.31", + "@types/pg": "~8.6.1", + "@types/sinon": "~10.0.4", + "@types/sinon-chai": "~3.2.5", + "@types/swagger-ui-express": "~4.1.3", + "@types/uuid": "~8.3.1", + "@types/xml2js": "^0.4.9", + "@types/yamljs": "~0.2.31", + "@typescript-eslint/eslint-plugin": "~4.33.0", + "@typescript-eslint/parser": "~4.33.0", + "chai": "~4.3.4", + "del": "~6.0.0", + "eslint": "~7.32.0", + "eslint-config-prettier": "~6.15.0", + "eslint-plugin-prettier": "~3.3.1", + "gulp": "~4.0.2", + "gulp-typescript": "~5.0.1", + "mocha": "~8.4.0", + "nodemon": "~2.0.14", + "npm-run-all": "~4.1.5", + "nyc": "~15.1.0", + "prettier": "~2.2.1", + "prettier-plugin-organize-imports": "~2.3.4", + "sinon": "~11.1.2", + "sinon-chai": "~3.7.0", + "ts-mocha": "~8.0.0", + "ts-node": "~10.4.0" + }, + "engines": { + "node": ">= 14.0.0", + "npm": ">= 6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.10" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.7" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.5" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.10" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "node_modules/@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "node_modules/@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "node_modules/@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@babel/types/node_modules/@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@elastic/elasticsearch": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.1.0.tgz", + "integrity": "sha512-IiZ6u77C7oYYbUkx/YFgEJk6ZtP+QDI97VaUWiYD14xIdn/w9WJtmx/Y1sN8ov0nZzrWbqScB2Z7Pb8oxo7vqw==", + "dependencies": { + "@elastic/transport": "^8.0.2", + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@elastic/elasticsearch/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/@elastic/transport": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.0.2.tgz", + "integrity": "sha512-OlDz3WO3pKE9vSxW4wV/mn7rYCtBmSsDwxr64h/S1Uc/zrIBXb0iUsRMSkiybXugXhjwyjqG2n1Wc7jjFxrskQ==", + "dependencies": { + "debug": "^4.3.2", + "hpagent": "^0.1.2", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.3.0", + "undici": "^4.14.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@elastic/transport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@elastic/transport/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@elastic/transport/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@elastic/transport/node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.1.tgz", + "integrity": "sha512-/gz6LgVpky205LuoOfwEZmnUtaSmdk0QIMcNFj9OvxhiMhPpKftMgZmGN7jNj7jR+lr8IB1Yks3QSSSNSxfoaQ==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15", + "source-map-support": "*", + "ts-node": "*" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/adm-zip": { + "version": "0.4.34", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz", + "integrity": "sha512-8ToYLLAYhkRfcmmljrKi22gT2pqu7hGMDtORP1emwIEGmgUTZOsaDjzWFzW5N2frcFRz/50CWt4zA1CxJ73pmQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "dev": true + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "dependencies": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/express-unless": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/glob-stream": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-6.1.1.tgz", + "integrity": "sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==", + "dev": true, + "dependencies": { + "@types/glob": "*", + "@types/node": "*" + } + }, + "node_modules/@types/gulp": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/gulp/-/gulp-4.0.9.tgz", + "integrity": "sha512-zzT+wfQ8uwoXjDhRK9Zkmmk09/fbLLmN/yDHFizJiEKIve85qutOnXcP/TM2sKPBTU+Jc16vfPbOMkORMUBN7Q==", + "dev": true, + "dependencies": { + "@types/undertaker": "*", + "@types/vinyl-fs": "*", + "chokidar": "^3.3.1" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true, + "optional": true + }, + "node_modules/@types/jsonpath": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.0.tgz", + "integrity": "sha512-v7qlPA0VpKUlEdhghbDqRoKMxFB3h3Ch688TApBJ6v+XLDdvWCGLJIYiPKGZnS6MAOie+IorCfNYVHOPIHSWwQ==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz", + "integrity": "sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.14.176", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", + "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", + "dev": true + }, + "node_modules/@types/multer": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "14.14.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.45.tgz", + "integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==" + }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static/node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/sinon": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.4.tgz", + "integrity": "sha512-fOYjrxQv8zJsqOY6V6ecP4eZhQBxtY80X0er1VVnUIAIZo74jHm8e1vguG5Yt4Iv8W2Wr7TgibB8MfRe32k9pA==", + "dev": true, + "dependencies": { + "@sinonjs/fake-timers": "^7.1.0" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.5.tgz", + "integrity": "sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==", + "dev": true, + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz", + "integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/undertaker": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.7.tgz", + "integrity": "sha512-xuY7nBwo1zSRoY2aitp/HArHfTulFAKql2Fr4b4mWbBBP+F50n7Jm6nwISTTMaDk2xvl92O10TTejVF0Q9mInw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/undertaker-registry": "*", + "async-done": "~1.3.2" + } + }, + "node_modules/@types/undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", + "dev": true + }, + "node_modules/@types/vinyl": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz", + "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==", + "dev": true, + "dependencies": { + "@types/expect": "^1.20.4", + "@types/node": "*" + } + }, + "node_modules/@types/vinyl-fs": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-2.4.12.tgz", + "integrity": "sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==", + "dev": true, + "dependencies": { + "@types/glob-stream": "*", + "@types/node": "*", + "@types/vinyl": "*" + } + }, + "node_modules/@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yamljs": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", + "integrity": "sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^4.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/adm-zip": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.5.tgz", + "integrity": "sha512-IWwXKnCbirdbyXSfUDvCCrmYrOHANRZcc8NcRrvTlIApdl7PwE9oGcsYvNeJPAVY1M+70b4PxXGKIf8AEuiQ6w==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", + "dev": true, + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE=", + "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", + "dev": true, + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", + "dev": true, + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", + "dev": true, + "dependencies": { + "array-slice": "^1.0.0", + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-initial/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", + "dev": true, + "dependencies": { + "is-number": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-last/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", + "dev": true, + "dependencies": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-sort/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + }, + "node_modules/async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", + "dev": true, + "dependencies": { + "async-done": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/aws-sdk": { + "version": "2.742.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.742.0.tgz", + "integrity": "sha512-zntDB0BpMn/y+B4RQvXuqY8DmJDYPkeFjZ6BbZ6vdNrsdB5TRz8p53ats4D3mLG068RB4M4AmVioFnU69nDXyQ==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/aws-sdk/node_modules/xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "node_modules/aws-sdk/node_modules/xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", + "dev": true, + "dependencies": { + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "dependencies": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/busboy/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/busboy/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/busboy/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cfb": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.1.tgz", + "integrity": "sha512-wT2ScPAFGSVy7CY+aauMezZBnNrfnaLSrxHUHdea+Td/86vrk6ZquggV+ssBR88zNs0OnBkL2+lf9q0K+zVGzQ==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0", + "printj": "~1.3.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.0.tgz", + "integrity": "sha512-f5nltvjl+PRUh6YNfUstRaXwJxtfnKEWhAWWlmKvh+Y3J2+98a0KKVYDEhz6NdKGqswLhjNGznxfSsZGOvOd9g==", + "dependencies": { + "printj": "~1.2.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32/node_modules/printj": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.2.3.tgz", + "integrity": "sha512-sanczS6xOJOg7IKDvi4sGOUOe7c1tsEzjwlLFH/zgwx/uyImVM9/rgBkc8AfiQa/Vg54nRd8mkm9yI7WV/O+WA==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/printj": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz", + "integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/chokidar/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/clamdjs": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clamdjs/-/clamdjs-1.0.2.tgz", + "integrity": "sha512-gVnX5ySMULvwYL2ykZQnP4UK4nIK7ftG6z015drJyOFgWpsqXt1Hcq4fMyPwM8LLsxfgfYKLiZi288xuTfmZBQ==" + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/cloneable-readable/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/cloneable-readable/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "dev": true, + "dependencies": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "dependencies": { + "color": "3.0.x", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/convert-source-map/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-props": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "dev": true, + "dependencies": { + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + } + }, + "node_modules/copy-props/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/db-migrate": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.11.11.tgz", + "integrity": "sha512-GHZodjB5hXRy+76ZIb9z0OrUn0qSeGfvS0cCfyzPeFCBZ1YU9o9HUBQ8pUT+v/fJ9+a29eRz2xQsLfccXZtf8g==", + "dependencies": { + "balanced-match": "^1.0.0", + "bluebird": "^3.1.1", + "db-migrate-shared": "^1.2.0", + "deep-extend": "^0.6.0", + "dotenv": "^5.0.1", + "final-fs": "^1.6.0", + "inflection": "^1.10.0", + "mkdirp": "~0.5.0", + "parse-database-url": "~0.3.0", + "prompt": "^1.0.0", + "rc": "^1.2.8", + "resolve": "^1.1.6", + "semver": "^5.3.0", + "tunnel-ssh": "^4.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "db-migrate": "bin/db-migrate" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-db-migrate" + } + }, + "node_modules/db-migrate-base": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/db-migrate-base/-/db-migrate-base-2.3.0.tgz", + "integrity": "sha512-mxaCkSe7JC2uksvI/rKs+wOQGBSZ6B87xa4b3i+QhB+XRBpGdpMzldKE6INf+EnM6kwhbIPKjyJZgyxui9xBfQ==", + "dependencies": { + "bluebird": "^3.1.1" + } + }, + "node_modules/db-migrate-pg": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/db-migrate-pg/-/db-migrate-pg-1.2.2.tgz", + "integrity": "sha512-+rgrhGNWC2SzcfweopyZqOQ1Igz1RVFMUZwUs6SviHpOUzFwb0NZWkG0pw1GaO+JxTxS7VJjckUWkOwZbVYVag==", + "dependencies": { + "bluebird": "^3.1.1", + "db-migrate-base": "^2.3.0", + "pg": "^8.0.3", + "semver": "^5.0.3" + } + }, + "node_modules/db-migrate-shared": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/db-migrate-shared/-/db-migrate-shared-1.2.0.tgz", + "integrity": "sha512-65k86bVeHaMxb2L0Gw3y5V+CgZSRwhVQMwDMydmw5MvIpHHwD6SmBciqIwHsZfzJ9yzV/yYhdRefRM6FV5/siw==" + }, + "node_modules/db-migrate/node_modules/dotenv": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", + "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==", + "engines": { + "node": ">=4.6.0" + } + }, + "node_modules/debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-equal": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", + "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "node_modules/default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, + "dependencies": { + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-compare/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-require-extensions/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "dependencies": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/dicer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/dicer/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/dicer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/difunc": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/difunc/-/difunc-0.0.4.tgz", + "integrity": "sha512-zBiL4ALDmviHdoLC0g0G6wVme5bwAow9WfhcZLLopXCAWgg3AEf7RYTs2xugszIGulRHzEVDF/SHl9oyQU07Pw==", + "dependencies": { + "esprima": "^4.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "dependencies": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/escodegen/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", + "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-visitor-keys": { + "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, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-normalize-query-params-middleware": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/express-normalize-query-params-middleware/-/express-normalize-query-params-middleware-0.5.1.tgz", + "integrity": "sha1-2+HoE5rssjT7attcAFnHXblzPSo=" + }, + "node_modules/express-openapi": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/express-openapi/-/express-openapi-9.3.0.tgz", + "integrity": "sha512-92H8nuvO1vVMutapDqQXESOxFnaC4/tZAXSi7kJMD+xWXZwNwmuinCxbfQc7JyUY6Y3+vjFXqJ7xeTCpsUhSiA==", + "dependencies": { + "express-normalize-query-params-middleware": "^0.5.0", + "openapi-framework": "^9.3.0", + "openapi-types": "^9.3.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "dependencies": { + "type": "^2.0.0" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "engines": { + "node": "> 0.1.90" + } + }, + "node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "dev": true, + "dependencies": { + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "node_modules/fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, + "node_modules/fastq": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.0.tgz", + "integrity": "sha512-NL2Qc5L3iQEsyYzweq7qfgy5OtXCmGzGvhElGEd/SoFWEMOEczNh5s5ocaF01HDetxz+p8ecjNPA6cZxxIHmzA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" + }, + "node_modules/fflate": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.1.tgz", + "integrity": "sha512-VYM2Xy1gSA5MerKzCnmmuV2XljkpKwgJBKezW+495TTnTCh1x5HcYa1aH8wRU/MfTGhW4ziXqgwprgQUVl3Ohw==" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/final-fs": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/final-fs/-/final-fs-1.6.1.tgz", + "integrity": "sha1-1tzZLvb+T+jAer1WjHE1YQ7eMjY=", + "dependencies": { + "node-fs": "~0.1.5", + "when": "~2.0.1" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/flush-write-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/flush-write-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/flush-write-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fs-routes": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/fs-routes/-/fs-routes-9.0.3.tgz", + "integrity": "sha512-Y5tkylY9fQ1jm11FdJoptzqIG3OyzqrOF16W5odNlIdqFqb2355IbNB3jQkE+C268mSShLmIur8ynYCgL/Yg/g==", + "peerDependencies": { + "glob": ">=7.1.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", + "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-stream/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-stream/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/glob-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/glob-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/glob-watcher": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-watcher/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/glob-watcher/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/glob-watcher/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-watcher/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/glob-watcher/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/glob-watcher/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/glob-watcher/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globals": { + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, + "dependencies": { + "sparkles": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "engines": { + "node": ">=4.x" + } + }, + "node_modules/gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "dev": true, + "dependencies": { + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "typescript": "~2.7.1 || >=2.8.0-dev || >=2.9.0-dev || ~3.0.0 || >=3.0.0-dev || >=3.1.0-dev || >= 3.2.0-dev || >= 3.3.0-dev" + } + }, + "node_modules/gulp-typescript/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-typescript/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/gulp-typescript/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/gulp-typescript/node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, + "node_modules/gulp/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "node_modules/gulp/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "node_modules/gulp/node_modules/gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp/node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/gulp/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "node_modules/gulp/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "node_modules/gulp/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "dependencies": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp/node_modules/y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "node_modules/gulp/node_modules/yargs": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", + "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "5.0.0-security.0" + } + }, + "node_modules/gulp/node_modules/yargs-parser": { + "version": "5.0.0-security.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", + "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "dev": true, + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + }, + "node_modules/gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true, + "dependencies": { + "glogg": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/hpagent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", + "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/i": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", + "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=", + "engines": [ + "node >= 0.4.0" + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-dir/-/is-dir-1.0.0.tgz", + "integrity": "sha1-QdN/SV/MrMBaR3jWboMCTCkro/8=" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "dependencies": { + "@panva/asn1.js": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0 < 13 || >=13.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", + "dev": true + }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", + "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", + "dependencies": { + "@types/express-jwt": "0.0.42", + "debug": "^4.3.2", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.4" + }, + "engines": { + "node": ">=10 < 13 || >=14" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/knex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-1.0.1.tgz", + "integrity": "sha512-pusgMo74lEbUxmri+YfWV8x/LJacP/2KcemTCKH7WnXFYz5RoMi+8WM4OJ05b0glfF+aWB4nkFsxsXxJ8qioLQ==", + "dependencies": { + "colorette": "2.0.16", + "commander": "^8.3.0", + "debug": "4.3.3", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.5.0", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependenciesMeta": { + "@vscode/sqlite3": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/knex/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/knex/node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", + "dev": true, + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "dependencies": { + "flush-write-stream": "^1.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "dependencies": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "dependencies": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "node_modules/lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", + "dev": true, + "dependencies": { + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/matchdep/node_modules/findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "dev": true, + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 10.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/mocha/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/mocha/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/moment": { + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "engines": { + "node": "*" + } + }, + "node_modules/mongodb-uri": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz", + "integrity": "sha1-D3ca0W9IOuZfQoeWlCjp+8SqYYE=", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/multer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "dev": true, + "optional": true + }, + "node_modules/nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/ncp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-1.0.1.tgz", + "integrity": "sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=", + "bin": { + "ncp": "bin/ncp" + } + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/node-fs": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/node-fs/-/node-fs-0.1.7.tgz", + "integrity": "sha1-MjI8zLRsn78PwRgS1FAhzDHTJbs=", + "os": [ + "linux", + "darwin", + "freebsd", + "win32", + "smartos", + "sunos" + ], + "engines": { + "node": ">=0.1.97" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nodemon": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", + "integrity": "sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "chokidar": "^3.2.2", + "debug": "^3.2.6", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.0.4", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.3", + "update-notifier": "^5.1.0" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "dependencies": { + "once": "^1.3.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openapi-default-setter": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-default-setter/-/openapi-default-setter-9.3.0.tgz", + "integrity": "sha512-Y4PtlmeStp43dyy4x+ekibGrT/LYIz6Y9gnSJ0arELX/xc5uyTC7C2qJgeXf4RJcHW+yB9Q9QvyLUNDSa+8oFg==", + "dependencies": { + "openapi-types": "^9.3.0" + } + }, + "node_modules/openapi-framework": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-framework/-/openapi-framework-9.3.0.tgz", + "integrity": "sha512-mgeEqJcf18Fnd0MQ1I2T1fLljAtu6HkU0MknPM/IoVOXRDscKgQjzLIR/FyVfNcg358MXXsgUtVgDsbVQujyYA==", + "dependencies": { + "difunc": "0.0.4", + "fs-routes": "^9.0.3", + "glob": "*", + "is-dir": "^1.0.0", + "js-yaml": "^3.10.0", + "openapi-default-setter": "^9.3.0", + "openapi-request-coercer": "^9.3.0", + "openapi-request-validator": "^9.3.0", + "openapi-response-validator": "^9.3.0", + "openapi-schema-validator": "^9.3.0", + "openapi-security-handler": "^9.3.0", + "openapi-types": "^9.3.0", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-jsonschema-parameters": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-9.3.0.tgz", + "integrity": "sha512-tUNAtzlJm5YaoqQMKvonRZN0BWRVRd34ulmGgzMLL+Ga23VnSy3FyFFI46LDUeIbh9wS2NGjkuO4akE01u7Rmw==", + "dependencies": { + "openapi-types": "^9.3.0" + } + }, + "node_modules/openapi-request-coercer": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-request-coercer/-/openapi-request-coercer-9.3.0.tgz", + "integrity": "sha512-5EvH0KeRZ3ygDljPTWFEXKvW9ga4h6HGiZN29H7F4g/OQBdKyFMCRpyUQZeVauJbuk6K5mvL6TdsmqdqI3D2Bg==", + "dependencies": { + "openapi-types": "^9.3.0", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-request-validator": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-request-validator/-/openapi-request-validator-9.3.0.tgz", + "integrity": "sha512-SmpYM8HbCn6A22CS6ysvXItwWEpp/dJLqepCfh5F16S7Isy/7txbxGimM1xyhNZh+silXH8wjsac5jfbSniXgw==", + "dependencies": { + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", + "content-type": "^1.0.4", + "openapi-jsonschema-parameters": "^9.3.0", + "openapi-types": "^9.3.0", + "ts-log": "^2.1.4" + } + }, + "node_modules/openapi-response-validator": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-9.3.0.tgz", + "integrity": "sha512-pklr94TIvl/ObZ0Gs04ihYWSi6w4k7jAerw1rSBHklb/ZbFTS5iP1t753PdSW9/7QJdXzZP/9uMADkhyURNjwA==", + "dependencies": { + "ajv": "^8.4.0", + "openapi-types": "^9.3.0" + } + }, + "node_modules/openapi-schema-validator": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.3.0.tgz", + "integrity": "sha512-KlvgZMWTu+H1FHFSZNAGj369uXl3BD1nXSIq+sXlG6P+OrsAHd3YORx0ZEZ3WGdu2LQrPGmtowGQavYXL+PLwg==", + "dependencies": { + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", + "lodash.merge": "^4.6.1", + "openapi-types": "^9.3.0" + } + }, + "node_modules/openapi-security-handler": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-security-handler/-/openapi-security-handler-9.3.0.tgz", + "integrity": "sha512-loy+sdPxjb0OuzIj0cp45kowoLEQ8z6FF0QJBFxtfDttuDssTtQ3Vw5C2kAZ/6Qu6X1y6HT4DAYdDY3iJ3iMNw==", + "dependencies": { + "openapi-types": "^9.3.0" + } + }, + "node_modules/openapi-types": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.0.tgz", + "integrity": "sha512-sR23YjmuwDSMsQVZDHbV9mPgi0RyniQlqR0AQxTC2/F3cpSjRFMH3CFPjoWvNqhC4OxPkDYNb2l8Mc1Me6D/KQ==" + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/ordered-read-streams/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "dependencies": { + "lcid": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-database-url": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/parse-database-url/-/parse-database-url-0.3.0.tgz", + "integrity": "sha1-NpZmMh6SfJreY838Gqr2+zdFPQ0=", + "dependencies": { + "mongodb-uri": ">= 0.9.7" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pg": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", + "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.4.1", + "pg-protocol": "^1.5.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "pg-native": ">=2.0.0" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + }, + "node_modules/pgpass": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", + "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", + "dependencies": { + "split2": "^3.1.1" + } + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkginfo": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", + "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", + "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", + "dev": true, + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9" + } + }, + "node_modules/pretty-hrtime": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompt": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz", + "integrity": "sha1-jlcSPDlquYiJf7Mn/Trtw+c15P4=", + "dependencies": { + "colors": "^1.1.2", + "pkginfo": "0.x.x", + "read": "1.0.x", + "revalidator": "0.1.x", + "utile": "0.3.x", + "winston": "2.1.x" + }, + "engines": { + "node": ">= 0.6.6" + } + }, + "node_modules/prompt/node_modules/async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" + }, + "node_modules/prompt/node_modules/winston": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.1.1.tgz", + "integrity": "sha1-PJNJ0ZYgf9G9/51LxD73JRDjoS4=", + "dependencies": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "pkginfo": "0.3.x", + "stack-trace": "0.0.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prompt/node_modules/winston/node_modules/colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/prompt/node_modules/winston/node_modules/pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "node_modules/pupa": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", + "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "dev": true, + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qs": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", + "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "dev": true, + "dependencies": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/replace-homedir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", + "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1", + "is-absolute": "^1.0.0", + "remove-trailing-separator": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "dependencies": { + "value-or-function": "^3.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/revalidator": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", + "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "node_modules/secure-json-parse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", + "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "dev": true, + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-greatest-satisfied-range": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", + "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", + "dev": true, + "dependencies": { + "sver-compat": "^1.5.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel/node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sinon": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "peerDependencies": { + "chai": "^4.0.0", + "sinon": ">=4.0.0" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/sparkles": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", + "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/spawn-wrap/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "dev": true + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "node_modules/sql-template-strings": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz", + "integrity": "sha1-PxFQiiWt384hejBCqdMAwxk7lv8=", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ssh2": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.5.4.tgz", + "integrity": "sha1-G/a2soyW6u8mf01sRqWiUXpZnic=", + "dependencies": { + "ssh2-streams": "~0.1.15" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssh2-streams": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.1.20.tgz", + "integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=", + "dependencies": { + "asn1": "~0.2.0", + "semver": "^5.1.0", + "streamsearch": "~0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "engines": { + "node": "*" + } + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "node_modules/streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/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==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz", + "integrity": "sha512-eCzTASPnoCr5Ht+Vn1YXgm8SB015hHKgEIMu9Nr9bQmLhRBxKRfmzSj/IQsxDFc8JInJDDFA0qXwK+xxI7wDkg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sver-compat": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", + "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", + "dev": true, + "dependencies": { + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/swagger-ui-dist": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.3.0.tgz", + "integrity": "sha512-RY1c3y6uuHBTu4nZPXcvrv9cnKj6MbaNMZK1NDyGHrUbQOO5WmkuMo6wi93WFzSURJk0SboD1X9nM5CtQAu2Og==" + }, + "node_modules/swagger-ui-express": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", + "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", + "dependencies": { + "swagger-ui-dist": ">=4.1.3" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0" + } + }, + "node_modules/table": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "dev": true, + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, + "node_modules/ts-log": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.3.tgz", + "integrity": "sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==" + }, + "node_modules/ts-mocha": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz", + "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==", + "dev": true, + "dependencies": { + "ts-node": "7.0.1" + }, + "bin": { + "ts-mocha": "bin/ts-mocha" + }, + "engines": { + "node": ">= 6.X.X" + }, + "optionalDependencies": { + "tsconfig-paths": "^3.5.0" + }, + "peerDependencies": { + "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X" + } + }, + "node_modules/ts-mocha/node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ts-mocha/node_modules/ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "dependencies": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "bin": { + "ts-node": "dist/bin.js" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "optional": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tunnel-ssh": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tunnel-ssh/-/tunnel-ssh-4.1.4.tgz", + "integrity": "sha512-CjBqboGvAbM7iXSX2F95kzoI+c2J81YkrHbyyo4SWNKCzU6w5LfEvXBCHu6PPriYaNvfhMKzD8bFf5Vl14YTtg==", + "dependencies": { + "debug": "2.6.9", + "lodash.defaults": "^4.1.0", + "ssh2": "0.5.4" + } + }, + "node_modules/tunnel-ssh/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, + "node_modules/undertaker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", + "integrity": "sha512-/RXwi5m/Mu3H6IHQGww3GNt1PNXlbeCuclF2QYR14L/2CHPz3DFZkvB5hZ0N/QUkiXWCACML2jXViIQEQc2MLg==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "bach": "^1.0.0", + "collection-map": "^1.0.0", + "es6-weak-map": "^2.0.1", + "fast-levenshtein": "^1.0.0", + "last-run": "^1.1.0", + "object.defaults": "^1.0.0", + "object.reduce": "^1.0.0", + "undertaker-registry": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker-registry": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", + "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/undertaker/node_modules/fast-levenshtein": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", + "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", + "dev": true + }, + "node_modules/undici": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-4.16.0.tgz", + "integrity": "sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw==", + "engines": { + "node": ">=12.18" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-notifier": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", + "dev": true, + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/update-notifier/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/update-notifier/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/utile": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/utile/-/utile-0.3.0.tgz", + "integrity": "sha1-E1LDQOuCDk2N26A5pPv6oy7U7zo=", + "dependencies": { + "async": "~0.9.0", + "deep-equal": "~0.2.1", + "i": "0.3.x", + "mkdirp": "0.x.x", + "ncp": "1.0.x", + "rimraf": "2.x.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-fs/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/vinyl-fs/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/vinyl-fs/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "dev": true, + "dependencies": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl-sourcemap/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/when": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/when/-/when-2.0.1.tgz", + "integrity": "sha1-jYcv4V5oQkyRtLck6EjggH2rZkI=" + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "dev": true, + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "dependencies": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "dependencies": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + }, + "engines": { + "node": ">= 6.4.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/winston-transport/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/winston-transport/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/winston/node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/xlsx": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.3.tgz", + "integrity": "sha512-dGZKfyPSXfnoITruwisuDVZkvnxhjgqzWJXBJm2Khmh01wcw8//baRUvhroVRhW2SLbnlpGcCZZbeZO1qJgMIw==", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.15.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.7.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.10.4", @@ -276,6 +12695,21 @@ } } }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", + "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, "@dabh/diagnostics": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", @@ -286,6 +12720,128 @@ "kuler": "^2.0.0" } }, + "@elastic/elasticsearch": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.1.0.tgz", + "integrity": "sha512-IiZ6u77C7oYYbUkx/YFgEJk6ZtP+QDI97VaUWiYD14xIdn/w9WJtmx/Y1sN8ov0nZzrWbqScB2Z7Pb8oxo7vqw==", + "requires": { + "@elastic/transport": "^8.0.2", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@elastic/transport": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.0.2.tgz", + "integrity": "sha512-OlDz3WO3pKE9vSxW4wV/mn7rYCtBmSsDwxr64h/S1Uc/zrIBXb0iUsRMSkiybXugXhjwyjqG2n1Wc7jjFxrskQ==", + "requires": { + "debug": "^4.3.2", + "hpagent": "^0.1.2", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.3.0", + "undici": "^4.14.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + } + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -348,6 +12904,11 @@ "fastq": "^1.6.0" } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -364,18 +12925,18 @@ } }, "@sinonjs/fake-timers": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.0.5.tgz", - "integrity": "sha512-fUt6b15bjV/VW93UP5opNXJxdwZSbK1EdiwnhN7XrQrcpaOhMJpZ/CjwFpM3THpxwA+YviBUJKSuEqKlCK5alw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/samsam": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", - "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.6.0", @@ -398,6 +12959,30 @@ "defer-to-connect": "^1.0.1" } }, + "@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, "@types/adm-zip": { "version": "0.4.34", "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.4.34.tgz", @@ -407,40 +12992,29 @@ "@types/node": "*" } }, - "@types/bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha1-osKL4CwIVfUm5DeF+jJvKCQC4pA=" - }, "@types/body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", + "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", "requires": { "@types/connect": "*", "@types/node": "*" } }, "@types/chai": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz", - "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==", + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", "dev": true }, "@types/connect": { - "version": "3.4.34", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", - "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "requires": { "@types/node": "*" } }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -448,12 +13022,12 @@ "dev": true }, "@types/express": { - "version": "4.17.9", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", - "integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==", + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "*", + "@types/express-serve-static-core": "^4.17.18", "@types/qs": "*", "@types/serve-static": "*" } @@ -468,9 +13042,9 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.15.tgz", - "integrity": "sha512-pb71P0BrBAx7cQE+/7QnA1HTQUkdBKMlkPY7lHUMn0YvPJkL2UA+KW3BdWQ309IT+i9En/qm45ZxpjIcpgEhNQ==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", + "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", "requires": { "@types/node": "*", "@types/qs": "*", @@ -478,23 +13052,23 @@ } }, "@types/express-unless": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", - "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", "requires": { "@types/express": "*" } }, "@types/geojson": { - "version": "7946.0.7", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", - "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==", + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", "dev": true }, "@types/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", "dev": true, "requires": { "@types/minimatch": "*", @@ -502,9 +13076,9 @@ } }, "@types/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-RHv6ZQjcTncXo3thYZrsbAVwoy4vSKosSWhuhuQxLOTv74OJuFQxXkmUuZCr3q9uNBEVCvIzmZL/FeRNbHZGUg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/glob-stream/-/glob-stream-6.1.1.tgz", + "integrity": "sha512-AGOUTsTdbPkRS0qDeyeS+6KypmfVpbT5j23SN8UPG63qjKXNKjXn6V9wZUr8Fin0m9l8oGYaPK8b2WUMF8xI1A==", "dev": true, "requires": { "@types/glob": "*", @@ -512,9 +13086,9 @@ } }, "@types/gulp": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@types/gulp/-/gulp-4.0.7.tgz", - "integrity": "sha512-AjvRWEMr6pl9yQ5Yyg+2tiv/n6Ifowpi+NjhRqGwpHWSHH21uXPMHEqKVUT3HGVguACOuzgtk9jtWjChSREPFQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/gulp/-/gulp-4.0.9.tgz", + "integrity": "sha512-zzT+wfQ8uwoXjDhRK9Zkmmk09/fbLLmN/yDHFizJiEKIve85qutOnXcP/TM2sKPBTU+Jc16vfPbOMkORMUBN7Q==", "dev": true, "requires": { "@types/undertaker": "*", @@ -523,9 +13097,9 @@ } }, "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, "@types/json5": { @@ -542,93 +13116,96 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.0.tgz", - "integrity": "sha512-9bVao7LvyorRGZCw0VmH/dr7Og+NdjYSsKAxB43OQoComFbBgsEpoR9JW6+qSq/ogwVBg8GI2MfAlk4SYI4OLg==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.5.tgz", + "integrity": "sha512-OGqtHQ7N5/Ap/TUwO6IgHDuLiAoTmHhGpNvgkCm/F4N6pKzx/RBSfr2OXZSwC6vkfnsEdb6+7DNZVtiXiwdwFw==", "dev": true, "requires": { "@types/node": "*" } }, "@types/lodash": { - "version": "4.14.173", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", - "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==", + "version": "4.14.176", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.176.tgz", + "integrity": "sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==", "dev": true }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true }, "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "dev": true }, "@types/mocha": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.4.tgz", - "integrity": "sha512-M4BwiTJjHmLq6kjON7ZoI2JMlBvpY3BYSdiP6s/qCT3jb1s9/DeJF0JELpAxiVSIxXDzfNKe+r7yedMIoLbknQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz", + "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==", "dev": true }, "@types/multer": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.5.tgz", - "integrity": "sha512-9b/0a8JyrR0r2nQhL73JR86obWL7cogfX12augvlrvcpciCo/hkvEsgu80Z4S2g2DHGVXHr8pUIi1VhqFJ8Ufw==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz", + "integrity": "sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==", "dev": true, "requires": { "@types/express": "*" } }, "@types/node": { - "version": "14.14.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.13.tgz", - "integrity": "sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ==" + "version": "14.14.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.45.tgz", + "integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==" }, "@types/pg": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.14.7.tgz", - "integrity": "sha512-ZnMOUidTP6Lwsb0bxHL6PVIL1lVC2CYNQWlA79kQ6nn0rK1/ynvkyN1wsR9pVZaP4WcCNioKT/2aU5UuLIQy2w==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", "dev": true, "requires": { "@types/node": "*", - "@types/pg-types": "*" + "pg-protocol": "*", + "pg-types": "^2.2.0" } }, - "@types/pg-types": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/@types/pg-types/-/pg-types-1.11.5.tgz", - "integrity": "sha512-L8ogeT6vDzT1vxlW3KITTCt+BVXXVkLXfZ/XNm6UqbcJgxf+KPO7yjWx7dQQE8RW07KopL10x2gNMs41+IkMGQ==", - "dev": true - }, "@types/qs": { - "version": "6.9.5", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", - "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==" + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" }, "@types/range-parser": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", - "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/serve-static": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", - "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "requires": { - "@types/mime": "*", + "@types/mime": "^1", "@types/node": "*" + }, + "dependencies": { + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + } } }, "@types/sinon": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.0.tgz", - "integrity": "sha512-jDZ55oCKxqlDmoTBBbBBEx+N8ZraUVhggMZ9T5t+6/Dh8/4NiOjSUfpLrPiEwxQDlAe3wpAkoXhWvE6LibtsMQ==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.4.tgz", + "integrity": "sha512-fOYjrxQv8zJsqOY6V6ecP4eZhQBxtY80X0er1VVnUIAIZo74jHm8e1vguG5Yt4Iv8W2Wr7TgibB8MfRe32k9pA==", "dev": true, "requires": { - "@sinonjs/fake-timers": "^7.0.4" + "@sinonjs/fake-timers": "^7.1.0" } }, "@types/sinon-chai": { @@ -641,15 +13218,20 @@ "@types/sinon": "*" } }, - "@types/swagger-schema-official": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/swagger-schema-official/-/swagger-schema-official-2.0.1.tgz", - "integrity": "sha1-xU9998/nBHdKbLI6zjOTd3o0rPA=" + "@types/swagger-ui-express": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.3.tgz", + "integrity": "sha512-jqCjGU/tGEaqIplPy3WyQg+Nrp6y80DCFnDEAvVKWkJyv0VivSSDCChkppHRHAablvInZe6pijDFMnavtN0vqA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/serve-static": "*" + } }, "@types/undertaker": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.5.tgz", - "integrity": "sha512-j0hCpPn9kdxdJX8eMTtFnlMrME0SK9T0PioDovo+6YaFWtkAZhvZlzMEKvOju2QRYM3bBCJQUzPbJ4bx/fxj2w==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/undertaker/-/undertaker-1.2.7.tgz", + "integrity": "sha512-xuY7nBwo1zSRoY2aitp/HArHfTulFAKql2Fr4b4mWbBBP+F50n7Jm6nwISTTMaDk2xvl92O10TTejVF0Q9mInw==", "dev": true, "requires": { "@types/node": "*", @@ -664,15 +13246,15 @@ "dev": true }, "@types/uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", "dev": true }, "@types/vinyl": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.4.tgz", - "integrity": "sha512-2o6a2ixaVI2EbwBPg1QYLGQoHK56p/8X/sGfKbFC8N6sY9lfjsMf/GprtkQkSya0D4uRiutRZ2BWj7k3JvLsAQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz", + "integrity": "sha512-ayJ0iOCDNHnKpKTgBG6Q6JOnHTj9zFta+3j2b8Ejza0e4cvRyMn0ZoLEmbPrTHe5YYRlDYPvPWVdV4cTaRyH7g==", "dev": true, "requires": { "@types/expect": "^1.20.4", @@ -680,9 +13262,9 @@ } }, "@types/vinyl-fs": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-2.4.11.tgz", - "integrity": "sha512-2OzQSfIr9CqqWMGqmcERE6Hnd2KY3eBVtFaulVo3sJghplUcaeMdL9ZjEiljcQQeHjheWY9RlNmumjIAvsBNaA==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/@types/vinyl-fs/-/vinyl-fs-2.4.12.tgz", + "integrity": "sha512-LgBpYIWuuGsihnlF+OOWWz4ovwCYlT03gd3DuLwex50cYZLmX3yrW+sFF9ndtmh7zcZpS6Ri47PrIu+fV+sbXw==", "dev": true, "requires": { "@types/glob-stream": "*", @@ -690,6 +13272,15 @@ "@types/vinyl": "*" } }, + "@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yamljs": { "version": "0.2.31", "resolved": "https://registry.npmjs.org/@types/yamljs/-/yamljs-0.2.31.tgz", @@ -697,23 +13288,25 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.7.1.tgz", - "integrity": "sha512-3DB9JDYkMrc8Au00rGFiJLK2Ja9CoMP6Ut0sHsXp3ZtSugjNxvSSHTnKLfo4o+QmjYBJqEznDqsG1zj4F2xnsg==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.7.1", - "debug": "^4.1.1", + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", + "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "ignore": "^5.1.8", + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -735,9 +13328,9 @@ "dev": true }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -752,62 +13345,154 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.7.1.tgz", - "integrity": "sha512-TqE97pv7HrqWcGJbLbZt1v59tcqsSVpWTOf1AqrWK7n8nok2sGgVtYRuGXeNeLw3wXlLEbY1MKP3saB2HsO/Ng==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/typescript-estree": "3.7.1", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" } }, "@typescript-eslint/parser": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.7.1.tgz", - "integrity": "sha512-W4QV/gXvfIsccN8225784LNOorcm7ch68Fi3V4Wg7gmkWSQRKevO4RrRqWo6N/Z/myK1QAiGgeaXN57m+R/8iQ==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.7.1", - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/typescript-estree": "3.7.1", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", + "debug": "^4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" } }, "@typescript-eslint/types": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.7.1.tgz", - "integrity": "sha512-PZe8twm5Z4b61jt7GAQDor6KiMhgPgf4XmUb9zdrwTbgtC/Sj29gXP1dws9yEn4+aJeyXrjsD9XN7AWFhmnUfg==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.7.1.tgz", - "integrity": "sha512-m97vNZkI08dunYOr2lVZOHoyfpqRs0KDpd6qkGaIcLGhQ2WPtgHOd/eVbsJZ0VYCQvupKrObAGTOvk3tfpybYA==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", "dev": true, "requires": { - "@typescript-eslint/types": "3.7.1", - "@typescript-eslint/visitor-keys": "3.7.1", - "debug": "^4.1.1", - "glob": "^7.1.6", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", + "debug": "^4.3.1", + "globby": "^11.0.3", "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" } }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -817,21 +13502,46 @@ "yallist": "^4.0.0" } }, + "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" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -841,12 +13551,13 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.7.1.tgz", - "integrity": "sha512-xn22sQbEya+Utj2IqJHGLA3i1jDzR43RzWupxojbSWnj3nnPLavaQmWe5utw03CwYao3r00qzXfgJMGNkrzrAA==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/types": "4.33.0", + "eslint-visitor-keys": "^2.0.0" } }, "@ungap/promise-all-settled": { @@ -877,9 +13588,16 @@ "dev": true }, "acorn-jsx": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, "adler-32": { @@ -907,70 +13625,31 @@ } }, "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" - }, - "dependencies": { - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - } + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" } }, "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, "requires": { - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } + "string-width": "^4.1.0" } }, "ansi-colors": { @@ -1008,9 +13687,9 @@ "dev": true }, "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -1063,7 +13742,8 @@ "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-filter": { "version": "1.1.2", @@ -1077,7 +13757,8 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true }, "arr-map": { "version": "2.0.2", @@ -1091,12 +13772,14 @@ "arr-union": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true }, "array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true }, "array-flatten": { "version": "1.1.1", @@ -1141,7 +13824,8 @@ "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==" + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true }, "array-sort": { "version": "1.0.0", @@ -1171,7 +13855,8 @@ "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true }, "arrify": { "version": "1.0.1", @@ -1196,12 +13881,13 @@ "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -1236,16 +13922,11 @@ "async-done": "^1.2.2" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true }, "aws-sdk": { "version": "2.742.0", @@ -1267,6 +13948,20 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } }, @@ -1304,6 +13999,7 @@ "version": "0.11.2", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, "requires": { "cache-base": "^1.0.1", "class-utils": "^0.3.5", @@ -1318,6 +14014,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -1326,6 +14023,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1334,6 +14032,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -1342,6 +14041,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -1356,9 +14056,9 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, "bindings": { @@ -1409,44 +14109,68 @@ } }, "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", "dev": true, "requires": { "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" }, "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } } } @@ -1464,6 +14188,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, "requires": { "arr-flatten": "^1.1.0", "array-unique": "^0.3.2", @@ -1481,6 +14206,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -1565,6 +14291,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, "requires": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", @@ -1635,7 +14362,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.0" @@ -1653,33 +14379,55 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "cfb": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", - "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.1.tgz", + "integrity": "sha512-wT2ScPAFGSVy7CY+aauMezZBnNrfnaLSrxHUHdea+Td/86vrk6ZquggV+ssBR88zNs0OnBkL2+lf9q0K+zVGzQ==", "requires": { - "adler-32": "~1.2.0", + "adler-32": "~1.3.0", "crc-32": "~1.2.0", - "printj": "~1.1.2" + "printj": "~1.3.0" + }, + "dependencies": { + "adler-32": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.0.tgz", + "integrity": "sha512-f5nltvjl+PRUh6YNfUstRaXwJxtfnKEWhAWWlmKvh+Y3J2+98a0KKVYDEhz6NdKGqswLhjNGznxfSsZGOvOd9g==", + "requires": { + "printj": "~1.2.2" + }, + "dependencies": { + "printj": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.2.3.tgz", + "integrity": "sha512-sanczS6xOJOg7IKDvi4sGOUOe7c1tsEzjwlLFH/zgwx/uyImVM9/rgBkc8AfiQa/Vg54nRd8mkm9yI7WV/O+WA==" + } + } + }, + "printj": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.3.0.tgz", + "integrity": "sha512-017o8YIaz8gLhaNxRB9eBv2mWXI2CtzhPJALnQTP+OPpuUfP0RMWqr/mHCzqVeu1AQxfzSfAtAq66vKB8y7Lzg==" + } } }, "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", - "pathval": "^1.1.0", + "pathval": "^1.1.1", "type-detect": "^4.0.5" } }, "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.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1710,19 +14458,19 @@ "dev": true }, "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", "dev": true, "requires": { - "anymatch": "~3.1.1", + "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" + "readdirp": "~3.6.0" }, "dependencies": { "braces": { @@ -1740,7 +14488,16 @@ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "to-regex-range": "^5.0.1" + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" } }, "is-number": { @@ -1775,6 +14532,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "define-property": "^0.2.5", @@ -1786,6 +14544,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -1891,20 +14650,9 @@ "dev": true }, "codepage": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", - "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", - "requires": { - "commander": "~2.14.1", - "exit-on-epipe": "~1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", - "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" - } - } + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==" }, "collection-map": { "version": "1.0.0", @@ -1921,6 +14669,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, "requires": { "map-visit": "^1.0.0", "object-visit": "^1.0.0" @@ -1979,9 +14728,9 @@ "dev": true }, "colorette": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==" }, "colors": { "version": "1.4.0", @@ -1997,19 +14746,10 @@ "text-hex": "1.0.x" } }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" }, "commondir": { "version": "1.0.1", @@ -2020,7 +14760,8 @@ "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -2128,25 +14869,28 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true }, "copy-props": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", - "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", "dev": true, "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + } } }, "core-util-is": { @@ -2290,7 +15034,8 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true }, "decompress-response": { "version": "3.3.0", @@ -2384,6 +15129,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, "requires": { "is-descriptor": "^1.0.2", "isobject": "^3.0.1" @@ -2393,6 +15139,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2401,6 +15148,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -2409,6 +15157,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -2444,12 +15193,6 @@ } } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -2463,7 +15206,8 @@ "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=" + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true }, "dicer": { "version": "0.2.5", @@ -2742,6 +15486,11 @@ "es6-symbol": "^3.1.1" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -2815,28 +15564,32 @@ } }, "eslint": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.5.0.tgz", - "integrity": "sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q==", + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", "enquirer": "^2.3.5", - "eslint-scope": "^5.1.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^1.3.0", - "espree": "^7.2.0", - "esquery": "^1.2.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -2844,7 +15597,7 @@ "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", - "lodash": "^4.17.19", + "lodash.merge": "^4.6.2", "minimatch": "^3.0.4", "natural-compare": "^1.4.0", "optionator": "^0.9.1", @@ -2853,11 +15606,20 @@ "semver": "^7.2.1", "strip-ansi": "^6.0.0", "strip-json-comments": "^3.1.0", - "table": "^5.2.3", + "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2870,6 +15632,38 @@ "uri-js": "^4.2.2" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -2892,9 +15686,9 @@ } }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -2915,18 +15709,18 @@ } }, "eslint-config-prettier": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", - "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", "dev": true, "requires": { "get-stdin": "^6.0.0" } }, "eslint-plugin-prettier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", - "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", + "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0" @@ -2943,18 +15737,18 @@ } }, "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "eslint-visitor-keys": "^2.0.0" } }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "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 }, "esm": { @@ -2971,6 +15765,14 @@ "acorn": "^7.4.0", "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "esprima": { @@ -2988,9 +15790,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -3005,9 +15807,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -3041,6 +15843,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, "requires": { "debug": "^2.3.3", "define-property": "^0.2.5", @@ -3055,6 +15858,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -3063,6 +15867,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -3071,6 +15876,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -3081,6 +15887,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, "requires": { "homedir-polyfill": "^1.0.1" } @@ -3148,13 +15955,13 @@ "integrity": "sha1-2+HoE5rssjT7attcAFnHXblzPSo=" }, "express-openapi": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/express-openapi/-/express-openapi-7.0.2.tgz", - "integrity": "sha512-jQuqCLWx6aWLo5Z9CJA7tYGUE6mXE+I0c1hJTOkm6/O+MJ97tEVzrOxzTTCxUKq+E69TOTRxfkXKLDClgF3Pow==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/express-openapi/-/express-openapi-9.3.0.tgz", + "integrity": "sha512-92H8nuvO1vVMutapDqQXESOxFnaC4/tZAXSi7kJMD+xWXZwNwmuinCxbfQc7JyUY6Y3+vjFXqJ7xeTCpsUhSiA==", "requires": { "express-normalize-query-params-middleware": "^0.5.0", - "openapi-framework": "^7.0.2", - "openapi-types": "^7.0.1" + "openapi-framework": "^9.3.0", + "openapi-types": "^9.3.0" } }, "ext": { @@ -3177,12 +15984,14 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, "requires": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -3192,6 +16001,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -3202,6 +16012,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, "requires": { "array-unique": "^0.3.2", "define-property": "^1.0.0", @@ -3217,6 +16028,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -3225,6 +16037,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -3233,6 +16046,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -3241,6 +16055,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -3249,6 +16064,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -3347,7 +16163,8 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -3374,17 +16191,17 @@ "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" }, "fflate": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", - "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.1.tgz", + "integrity": "sha512-VYM2Xy1gSA5MerKzCnmmuV2XljkpKwgJBKezW+495TTnTCh1x5HcYa1aH8wRU/MfTGhW4ziXqgwprgQUVl3Ohw==" }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-uri-to-path": { @@ -3398,6 +16215,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-number": "^3.0.0", @@ -3409,6 +16227,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -3472,6 +16291,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, "requires": { "detect-file": "^1.0.0", "is-glob": "^4.0.0", @@ -3483,6 +16303,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, "requires": { "expand-tilde": "^2.0.2", "is-plain-object": "^2.0.3", @@ -3494,7 +16315,8 @@ "flagged-respawn": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true }, "flat": { "version": "5.0.2", @@ -3503,20 +16325,19 @@ "dev": true }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" }, "dependencies": { "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -3525,9 +16346,9 @@ } }, "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, "flush-write-stream": { @@ -3578,19 +16399,21 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true }, "for-own": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, "requires": { "for-in": "^1.0.1" } @@ -3605,23 +16428,6 @@ "signal-exit": "^3.0.2" } }, - "form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "formidable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true - }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -3636,6 +16442,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, "requires": { "map-cache": "^0.2.2" } @@ -3662,9 +16469,10 @@ } }, "fs-routes": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-routes/-/fs-routes-7.0.1.tgz", - "integrity": "sha512-kSAfx/P8oLSi5+tblecTETcJJ/Q+qL+xzGx4hns/+gHXMkTOZEzG73/2dBDW1FFy5+ZW080XoMaBAN2kCN55aQ==" + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/fs-routes/-/fs-routes-9.0.3.tgz", + "integrity": "sha512-Y5tkylY9fQ1jm11FdJoptzqIG3OyzqrOF16W5odNlIdqFqb2355IbNB3jQkE+C268mSShLmIur8ynYCgL/Yg/g==", + "requires": {} }, "fs.realpath": { "version": "1.0.0", @@ -3672,9 +16480,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "optional": true }, @@ -3710,7 +16518,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -3753,12 +16560,13 @@ "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true }, "getopts": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz", - "integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" }, "glob": { "version": "7.1.6", @@ -3998,18 +16806,18 @@ } }, "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", "dev": true, "requires": { - "ini": "1.3.7" + "ini": "2.0.0" }, "dependencies": { "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true } } @@ -4018,6 +16826,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, "requires": { "global-prefix": "^1.0.1", "is-windows": "^1.0.1", @@ -4028,6 +16837,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, "requires": { "expand-tilde": "^2.0.2", "homedir-polyfill": "^1.0.1", @@ -4037,12 +16847,20 @@ } }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.12.1", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", + "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" + }, + "dependencies": { + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } } }, "globby": { @@ -4341,13 +17159,13 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, "requires": { "get-value": "^2.0.6", "has-values": "^1.0.0", @@ -4358,6 +17176,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, "requires": { "is-number": "^3.0.0", "kind-of": "^4.0.0" @@ -4367,6 +17186,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4399,6 +17219,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, "requires": { "parse-passwd": "^1.0.0" } @@ -4409,6 +17230,11 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "hpagent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", + "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4535,6 +17361,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, "requires": { "is-relative": "^1.0.0", "is-windows": "^1.0.1" @@ -4544,6 +17371,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -4552,6 +17380,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4575,7 +17404,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true }, "is-callable": { "version": "1.2.2", @@ -4604,6 +17434,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -4612,6 +17443,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4628,6 +17460,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { "is-accessor-descriptor": "^0.1.6", "is-data-descriptor": "^0.1.4", @@ -4637,7 +17470,8 @@ "kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -4649,12 +17483,14 @@ "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -4665,18 +17501,19 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, "requires": { "is-extglob": "^2.1.1" } }, "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" } }, "is-negated-glob": { @@ -4692,15 +17529,16 @@ "dev": true }, "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz", + "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==", "dev": true }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -4709,6 +17547,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -4743,6 +17582,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -4760,6 +17600,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, "requires": { "is-unc-path": "^1.0.0" } @@ -4788,6 +17629,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, "requires": { "unc-path-regex": "^0.1.2" } @@ -4807,7 +17649,8 @@ "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true }, "is-yarn-global": { "version": "0.3.0", @@ -4823,12 +17666,14 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -4973,6 +17818,14 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", + "requires": { + "@panva/asn1.js": "^1.0.0" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5006,18 +17859,17 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", @@ -5091,69 +17943,29 @@ } }, "jwks-rsa": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.9.0.tgz", - "integrity": "sha512-UPCfQQg0s2kF2Ju6UFJrQH73f7MaVN/hKBnYBYOp+X9KN4y6TLChhLtaXS5nRKbZqshwVdrZ9OY63m/Q9CLqcg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", + "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", "requires": { "@types/express-jwt": "0.0.42", - "axios": "^0.19.2", - "debug": "^4.1.0", - "jsonwebtoken": "^8.5.1", + "debug": "^4.3.2", + "jose": "^2.0.5", "limiter": "^1.1.5", - "lru-memoizer": "^2.1.2", - "ms": "^2.1.2" + "lru-memoizer": "^2.1.4" }, "dependencies": { - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - } - }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -5178,39 +17990,72 @@ "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true }, "knex": { - "version": "0.21.13", - "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.13.tgz", - "integrity": "sha512-O3Zfc7ZHWe32q5k1Z8TqzmiGYVQ9+Tiqb4wP9tPF/ho9DUrHuuy5fLVDdkwDN0gHIr+q5t+XJzNW40DkmeL7lw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-1.0.1.tgz", + "integrity": "sha512-pusgMo74lEbUxmri+YfWV8x/LJacP/2KcemTCKH7WnXFYz5RoMi+8WM4OJ05b0glfF+aWB4nkFsxsXxJ8qioLQ==", "requires": { - "colorette": "1.2.1", - "commander": "^6.2.0", - "debug": "4.3.1", + "colorette": "2.0.16", + "commander": "^8.3.0", + "debug": "4.3.3", + "escalade": "^3.1.1", "esm": "^3.2.25", - "getopts": "2.2.5", + "getopts": "2.3.0", "interpret": "^2.2.0", - "liftoff": "3.1.0", - "lodash": "^4.17.20", - "pg-connection-string": "2.4.0", - "tarn": "^3.0.1", - "tildify": "2.0.0", - "v8flags": "^3.2.0" + "lodash": "^4.17.21", + "pg-connection-string": "2.5.0", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "requires": { + "has": "^1.0.3" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "requires": { + "resolve": "^1.20.0" + } + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" } } }, @@ -5311,6 +18156,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, "requires": { "extend": "^3.0.0", "findup-sync": "^3.0.0", @@ -5415,10 +18261,10 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, "log-symbols": { @@ -5465,9 +18311,9 @@ } }, "lru-memoizer": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.3.tgz", - "integrity": "sha512-DcAptVUrKHbyKfSpvthwHwD42bFBLSAhTXJf5PQunu4F0/Hzy41WTamvavUWqsOPps26D0l5534aFvcwEcYzDw==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", "requires": { "lodash.clonedeep": "^4.5.0", "lru-cache": "~4.0.0" @@ -5500,6 +18346,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, "requires": { "kind-of": "^6.0.2" } @@ -5507,12 +18354,14 @@ "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, "requires": { "object-visit": "^1.0.0" } @@ -5583,6 +18432,7 @@ "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -5632,14 +18482,15 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, "requires": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -5649,6 +18500,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, "requires": { "is-plain-object": "^2.0.4" } @@ -5664,83 +18516,101 @@ } }, "mocha": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", - "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.4.3", - "debug": "4.2.0", - "diff": "4.0.2", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.1.6", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.14.0", + "js-yaml": "4.0.0", "log-symbols": "4.0.0", "minimatch": "3.0.4", - "ms": "2.1.2", - "nanoid": "3.1.12", + "ms": "2.1.3", + "nanoid": "3.1.20", "serialize-javascript": "5.0.1", "strip-json-comments": "3.1.1", - "supports-color": "7.2.0", + "supports-color": "8.1.1", "which": "2.0.2", "wide-align": "1.1.3", - "workerpool": "6.0.2", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "fill-range": "^7.0.1" } }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.3.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "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" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, "escape-string-regexp": { @@ -5749,6 +18619,15 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5765,20 +18644,19 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", "dev": true, "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" } }, "locate-path": { @@ -5791,9 +18669,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "p-limit": { @@ -5814,24 +18692,13 @@ "p-limit": "^3.0.2" } }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "picomatch": "^2.2.1" } }, "strip-json-comments": { @@ -5841,14 +18708,23 @@ "dev": true }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { "has-flag": "^4.0.0" } }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5859,95 +18735,49 @@ } }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true } } }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" }, "mongodb-uri": { "version": "0.9.7", @@ -5960,14 +18790,14 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "multer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", - "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.3.tgz", + "integrity": "sha512-np0YLKncuZoTzufbkM6wEKp68EhWJXcU6fq6QqrSwkckd2LlMgd1UqhUJLj6NS/5sZ8dE8LYDWslsltJznnXlg==", "requires": { "append-field": "^1.0.0", "busboy": "^0.2.11", "concat-stream": "^1.5.2", - "mkdirp": "^0.5.1", + "mkdirp": "^0.5.4", "object-assign": "^4.1.1", "on-finished": "^2.3.0", "type-is": "^1.6.4", @@ -5986,22 +18816,23 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "dev": true, "optional": true }, "nanoid": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", - "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", "dev": true }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, "requires": { "arr-diff": "^4.0.0", "array-unique": "^0.3.2", @@ -6045,27 +18876,18 @@ "dev": true }, "nise": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", - "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/fake-timers": "^7.0.4", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", "path-to-regexp": "^1.7.0" }, "dependencies": { - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -6083,35 +18905,6 @@ } } }, - "nock": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", - "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "json-stringify-safe": "^5.0.1", - "lodash.set": "^4.3.2", - "propagate": "^2.0.0" - }, - "dependencies": { - "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" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "node-fs": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/node-fs/-/node-fs-0.1.7.tgz", @@ -6127,9 +18920,9 @@ } }, "nodemon": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz", - "integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.14.tgz", + "integrity": "sha512-frcpDx+PviKEQRSYzwhckuO2zoHcBYLHI754RE9z5h1RGtrngerc04mLpQQCPWBkH/2ObrX7We9YiwVSYZpFJQ==", "dev": true, "requires": { "chokidar": "^3.2.2", @@ -6141,7 +18934,7 @@ "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.3", - "update-notifier": "^4.1.0" + "update-notifier": "^5.1.0" }, "dependencies": { "debug": { @@ -6421,6 +19214,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, "requires": { "copy-descriptor": "^0.1.0", "define-property": "^0.2.5", @@ -6431,6 +19225,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -6439,6 +19234,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -6448,8 +19244,7 @@ "object-inspect": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", - "dev": true + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" }, "object-keys": { "version": "1.1.1", @@ -6461,6 +19256,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, "requires": { "isobject": "^3.0.0" } @@ -6481,6 +19277,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, "requires": { "array-each": "^1.0.1", "array-slice": "^1.0.0", @@ -6492,6 +19289,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, "requires": { "for-own": "^1.0.0", "make-iterator": "^1.0.0" @@ -6501,6 +19299,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, "requires": { "isobject": "^3.0.1" } @@ -6540,148 +19339,95 @@ } }, "openapi-default-setter": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/openapi-default-setter/-/openapi-default-setter-7.0.1.tgz", - "integrity": "sha512-O9jhaZPEEJzI1HSG3Yw5rOIC0EpZ9PjRJgtksXKuSMyEoxUDnl7zQ27LuFRR1ykSMVhMt8vHMrQBQIwLW8S0yQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-default-setter/-/openapi-default-setter-9.3.0.tgz", + "integrity": "sha512-Y4PtlmeStp43dyy4x+ekibGrT/LYIz6Y9gnSJ0arELX/xc5uyTC7C2qJgeXf4RJcHW+yB9Q9QvyLUNDSa+8oFg==", "requires": { - "openapi-types": "^7.0.1" + "openapi-types": "^9.3.0" } }, "openapi-framework": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/openapi-framework/-/openapi-framework-7.2.0.tgz", - "integrity": "sha512-rC4U+SIBVxoTujSIrk84PMquBwkNJfhYC7KTTDUUc7yfIyMVKRat5TOuMDyc49Ovsv+7bdkx1stf7d0N9LbtLg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-framework/-/openapi-framework-9.3.0.tgz", + "integrity": "sha512-mgeEqJcf18Fnd0MQ1I2T1fLljAtu6HkU0MknPM/IoVOXRDscKgQjzLIR/FyVfNcg358MXXsgUtVgDsbVQujyYA==", "requires": { "difunc": "0.0.4", - "fs-routes": "^7.0.1", + "fs-routes": "^9.0.3", "glob": "*", "is-dir": "^1.0.0", "js-yaml": "^3.10.0", - "openapi-default-setter": "^7.0.1", - "openapi-request-coercer": "^7.1.0", - "openapi-request-validator": "^7.2.0", - "openapi-response-validator": "^7.0.1", - "openapi-schema-validator": "^7.0.1", - "openapi-security-handler": "^7.0.1", - "openapi-types": "^7.0.1", + "openapi-default-setter": "^9.3.0", + "openapi-request-coercer": "^9.3.0", + "openapi-request-validator": "^9.3.0", + "openapi-response-validator": "^9.3.0", + "openapi-schema-validator": "^9.3.0", + "openapi-security-handler": "^9.3.0", + "openapi-types": "^9.3.0", "ts-log": "^2.1.4" } }, "openapi-jsonschema-parameters": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-7.0.2.tgz", - "integrity": "sha512-hCC8wsWu9qU/pWCUClAYmUyXRhAeXSZUCRV7NVlj/8+3fWrtTBwk8GKI2dRa5Up0yZ3pstGi3Ewzzuixbmh8sw==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-jsonschema-parameters/-/openapi-jsonschema-parameters-9.3.0.tgz", + "integrity": "sha512-tUNAtzlJm5YaoqQMKvonRZN0BWRVRd34ulmGgzMLL+Ga23VnSy3FyFFI46LDUeIbh9wS2NGjkuO4akE01u7Rmw==", "requires": { - "openapi-types": "^7.0.1" + "openapi-types": "^9.3.0" } }, "openapi-request-coercer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/openapi-request-coercer/-/openapi-request-coercer-7.1.0.tgz", - "integrity": "sha512-6nvSgvOvLYMkUBu3NbHQU6Lcol1WxDr0DsOe3oYHb2tZhokrNEuOF20QYPV+CGZYyEzc0f+Hdas774n5B0euLg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-request-coercer/-/openapi-request-coercer-9.3.0.tgz", + "integrity": "sha512-5EvH0KeRZ3ygDljPTWFEXKvW9ga4h6HGiZN29H7F4g/OQBdKyFMCRpyUQZeVauJbuk6K5mvL6TdsmqdqI3D2Bg==", "requires": { - "openapi-types": "^7.0.1", + "openapi-types": "^9.3.0", "ts-log": "^2.1.4" } }, "openapi-request-validator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/openapi-request-validator/-/openapi-request-validator-7.2.0.tgz", - "integrity": "sha512-LgXvKco6XR5SKr8QBaM6v0++QXY5MP2yvvKv0Ckutef3css9MAyIcokDsBj6DYYzNnjmFxPx4ntuY7CZTC2ZFA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-request-validator/-/openapi-request-validator-9.3.0.tgz", + "integrity": "sha512-SmpYM8HbCn6A22CS6ysvXItwWEpp/dJLqepCfh5F16S7Isy/7txbxGimM1xyhNZh+silXH8wjsac5jfbSniXgw==", "requires": { - "ajv": "^6.5.4", + "ajv": "^8.3.0", + "ajv-formats": "^2.1.0", "content-type": "^1.0.4", - "openapi-jsonschema-parameters": "^7.0.2", - "openapi-types": "^7.0.1", + "openapi-jsonschema-parameters": "^9.3.0", + "openapi-types": "^9.3.0", "ts-log": "^2.1.4" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } } }, "openapi-response-validator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-7.0.1.tgz", - "integrity": "sha512-Fxr9YdQ6s7/SIvvM888iWnc1GUn/fFxTaMFqHkUv0/eNCYoBfOwAKj9aptaRfL+BJXlsVdXWCJd3GWkwn8sIJA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-9.3.0.tgz", + "integrity": "sha512-pklr94TIvl/ObZ0Gs04ihYWSi6w4k7jAerw1rSBHklb/ZbFTS5iP1t753PdSW9/7QJdXzZP/9uMADkhyURNjwA==", "requires": { - "ajv": "^6.5.4", - "openapi-types": "^7.0.1" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } + "ajv": "^8.4.0", + "openapi-types": "^9.3.0" } }, "openapi-schema-validator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-7.0.1.tgz", - "integrity": "sha512-P/dmF14xWbyaFVcoS1Fs2tUP4AhJO+eEnZV+jbApeo3569/Z2fiki6Mb6Rs7cfi0ewNnV4L4HiYH+HPZaKWnjQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-schema-validator/-/openapi-schema-validator-9.3.0.tgz", + "integrity": "sha512-KlvgZMWTu+H1FHFSZNAGj369uXl3BD1nXSIq+sXlG6P+OrsAHd3YORx0ZEZ3WGdu2LQrPGmtowGQavYXL+PLwg==", "requires": { - "ajv": "^6.5.2", + "ajv": "^8.1.0", + "ajv-formats": "^2.0.2", "lodash.merge": "^4.6.1", - "openapi-types": "^7.0.1", - "swagger-schema-official": "2.0.0-bab6bed" - }, - "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - } + "openapi-types": "^9.3.0" } }, "openapi-security-handler": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/openapi-security-handler/-/openapi-security-handler-7.0.1.tgz", - "integrity": "sha512-fiRJE2Z5F0tY9QBssBX9g8Txtr0oj1BOU0nOZ6QHHXQdCYxebszGgcXD63uy0UJQwzwVOMs/AlCnKNVS/yMSEg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-security-handler/-/openapi-security-handler-9.3.0.tgz", + "integrity": "sha512-loy+sdPxjb0OuzIj0cp45kowoLEQ8z6FF0QJBFxtfDttuDssTtQ3Vw5C2kAZ/6Qu6X1y6HT4DAYdDY3iJ3iMNw==", "requires": { - "openapi-types": "^7.0.1" + "openapi-types": "^9.3.0" } }, "openapi-types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-7.0.1.tgz", - "integrity": "sha512-6pi4/Fw+JIW1HHda2Ij7LRJ5QJ8f6YzaXnsRA6m44BJz8nLq/j5gVFzPBKJo+uOFhAeHqZC/3uzhTpYPga3Q/A==" + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.0.tgz", + "integrity": "sha512-sR23YjmuwDSMsQVZDHbV9mPgi0RyniQlqR0AQxTC2/F3cpSjRFMH3CFPjoWvNqhC4OxPkDYNb2l8Mc1Me6D/KQ==" }, "optionator": { "version": "0.9.1", @@ -6841,6 +19587,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, "requires": { "is-absolute": "^1.0.0", "map-cache": "^0.2.0", @@ -6865,7 +19612,8 @@ "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true }, "parseurl": { "version": "1.3.3", @@ -6875,7 +19623,8 @@ "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true }, "path-dirname": { "version": "1.0.2", @@ -6908,6 +19657,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "dev": true, "requires": { "path-root-regex": "^0.1.0" } @@ -6915,7 +19665,8 @@ "path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "dev": true }, "path-to-regexp": { "version": "0.1.7", @@ -6929,29 +19680,36 @@ "dev": true }, "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, "pg": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", - "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", + "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", - "pg-connection-string": "^2.4.0", - "pg-pool": "^3.2.2", - "pg-protocol": "^1.4.0", + "pg-connection-string": "^2.5.0", + "pg-pool": "^3.4.1", + "pg-protocol": "^1.5.0", "pg-types": "^2.1.0", "pgpass": "1.x" + }, + "dependencies": { + "pg-connection-string": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" + } } }, "pg-connection-string": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", - "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", + "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" }, "pg-int8": { "version": "1.0.1", @@ -6959,14 +19717,15 @@ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" }, "pg-pool": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", - "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", + "requires": {} }, "pg-protocol": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", - "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", + "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" }, "pg-types": { "version": "2.2.0", @@ -7061,7 +19820,8 @@ "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true }, "postgres-array": { "version": "2.0.0", @@ -7113,6 +19873,13 @@ "fast-diff": "^1.1.2" } }, + "prettier-plugin-organize-imports": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", + "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", + "dev": true, + "requires": {} + }, "pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -7190,12 +19957,6 @@ } } }, - "propagate": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", - "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", - "dev": true - }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -7252,9 +20013,12 @@ } }, "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } }, "querystring": { "version": "0.2.0", @@ -7371,9 +20135,9 @@ } }, "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { "picomatch": "^2.2.1" @@ -7383,6 +20147,7 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, "requires": { "resolve": "^1.1.6" } @@ -7391,15 +20156,16 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, "requires": { "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "registry-auth-token": { @@ -7459,12 +20225,14 @@ "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true }, "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true }, "replace-ext": { "version": "1.0.1", @@ -7511,6 +20279,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, "requires": { "expand-tilde": "^2.0.0", "global-modules": "^1.0.0" @@ -7534,7 +20303,8 @@ "resolve-url": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true }, "responselike": { "version": "1.0.2", @@ -7548,7 +20318,8 @@ "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true }, "reusify": { "version": "1.0.4", @@ -7584,6 +20355,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, "requires": { "ret": "~0.1.10" } @@ -7598,6 +20370,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, + "secure-json-parse": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", + "integrity": "sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -7705,6 +20482,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, "requires": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -7716,6 +20494,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -7748,6 +20527,28 @@ "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "dependencies": { + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + } + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -7763,27 +20564,24 @@ } }, "sinon": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-10.0.0.tgz", - "integrity": "sha512-XAn5DxtGVJBlBWYrcYKEhWCz7FLwZGdyvANRyK06419hyEpdT0dMc5A8Vcxg5SCGHc40CsqoKsc1bt1CbJPfNw==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.2.tgz", + "integrity": "sha512-59237HChms4kg7/sXhiRcUzdSkKuydDeTiamT/jesUVHshBgL8XAmhgFo0GfK6RruMDM/iRSij1EybmMog9cJw==", "dev": true, "requires": { - "@sinonjs/commons": "^1.8.1", - "@sinonjs/fake-timers": "^6.0.1", - "@sinonjs/samsam": "^5.3.1", - "diff": "^4.0.2", - "nise": "^4.1.0", - "supports-color": "^7.1.0" + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^7.1.2", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" }, "dependencies": { - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true }, "has-flag": { "version": "4.0.0", @@ -7803,10 +20601,11 @@ } }, "sinon-chai": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.6.0.tgz", - "integrity": "sha512-bk2h+0xyKnmvazAnc7HE5esttqmCerSMcBtuB2PS2T4tG6x8woXAxZeJaOJWD+8reXHngnXn0RtIbfEW9OTHFg==", - "dev": true + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", + "dev": true, + "requires": {} }, "slash": { "version": "3.0.0", @@ -7815,52 +20614,21 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" } }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, "requires": { "base": "^0.11.1", "debug": "^2.2.0", @@ -7876,6 +20644,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "requires": { "ms": "2.0.0" } @@ -7884,6 +20653,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -7892,6 +20662,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, "requires": { "is-extendable": "^0.1.0" } @@ -7902,6 +20673,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, "requires": { "define-property": "^1.0.0", "isobject": "^3.0.0", @@ -7912,6 +20684,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, "requires": { "is-descriptor": "^1.0.0" } @@ -7920,6 +20693,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -7928,6 +20702,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, "requires": { "kind-of": "^6.0.0" } @@ -7936,6 +20711,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, "requires": { "is-accessor-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0", @@ -7948,6 +20724,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, "requires": { "kind-of": "^3.2.0" }, @@ -7956,6 +20733,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -7965,12 +20743,14 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, "requires": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0", @@ -8000,7 +20780,8 @@ "source-map-url": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true }, "sparkles": { "version": "1.0.1", @@ -8078,6 +20859,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "requires": { "extend-shallow": "^3.0.0" } @@ -8143,6 +20925,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, "requires": { "define-property": "^0.2.5", "object-copy": "^0.1.0" @@ -8152,6 +20935,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { "is-descriptor": "^0.1.0" } @@ -8180,6 +20964,14 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -8221,14 +21013,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -8251,67 +21035,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, - "superagent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", - "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", - "dev": true, - "requires": { - "component-emitter": "^1.3.0", - "cookiejar": "^2.1.2", - "debug": "^4.1.1", - "fast-safe-stringify": "^2.0.7", - "form-data": "^3.0.0", - "formidable": "^1.2.2", - "methods": "^1.1.2", - "mime": "^2.4.6", - "qs": "^6.9.4", - "readable-stream": "^3.6.0", - "semver": "^7.3.2" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "mime": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", - "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==", - "dev": true - }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } - }, - "supertest": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.0.1.tgz", - "integrity": "sha512-8yDNdm+bbAN/jeDdXsRipbq9qMpVF7wRsbwLgsANHqdjPsCoecmlTuqEcLQMGpmojFBhxayZ0ckXmLXYq7e+0g==", - "dev": true, - "requires": { - "methods": "1.1.2", - "superagent": "6.1.0" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -8321,6 +21044,11 @@ "has-flag": "^3.0.0" } }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, "sver-compat": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", @@ -8331,113 +21059,64 @@ "es6-symbol": "^3.1.1" } }, - "swagger-object-validator": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/swagger-object-validator/-/swagger-object-validator-1.2.2.tgz", - "integrity": "sha512-cI9lVOyzKHXDQY0DwNBNM/DfW6xQzT4sDS3pcjfdLuSYAkOZpmpXGitRI6dkm+xIrONKZz4oNODmrs8rjOoQ3g==", + "swagger-ui-dist": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.3.0.tgz", + "integrity": "sha512-RY1c3y6uuHBTu4nZPXcvrv9cnKj6MbaNMZK1NDyGHrUbQOO5WmkuMo6wi93WFzSURJk0SboD1X9nM5CtQAu2Og==" + }, + "swagger-ui-express": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.3.0.tgz", + "integrity": "sha512-jN46SEEe9EoXa3ZgZoKgnSF6z0w3tnM1yqhO4Y+Q4iZVc8JOQB960EZpIAz6rNROrDApVDwcMHR0mhlnc/5Omw==", "requires": { - "@types/bluebird": "3.5.3", - "@types/swagger-schema-official": "2.0.1", - "bluebird": "^3.5.0", - "js-yaml": "3.13.1" - }, - "dependencies": { - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } + "swagger-ui-dist": ">=4.1.3" } }, - "swagger-schema-official": { - "version": "2.0.0-bab6bed", - "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", - "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" - }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" } } } }, "tarn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", - "integrity": "sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==" - }, - "term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" }, "test-exclude": { "version": "6.0.0", @@ -8544,6 +21223,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, "requires": { "kind-of": "^3.0.2" }, @@ -8552,6 +21232,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, "requires": { "is-buffer": "^1.1.5" } @@ -8568,6 +21249,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, "requires": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", @@ -8579,6 +21261,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, "requires": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" @@ -8652,19 +21335,31 @@ } }, "ts-node": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", - "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", "arg": "^4.1.0", "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "source-map-support": "^0.5.17", "yn": "3.1.1" }, "dependencies": { + "acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -8712,9 +21407,9 @@ "dev": true }, "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -8791,34 +21486,21 @@ } }, "typescript": { - "version": "3.9.8", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.8.tgz", - "integrity": "sha512-nDbnFkUZZjkQ92qwKX+C+jtk4OGfU8H9toSEs3uAsl8cxLjG2sqQm6leF/pLWvm9FAEJ6KHkYMAbHYaY2ITeVg==" + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==" }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true }, "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", - "dev": true, - "requires": { - "debug": "^2.2.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true }, "underscore": { "version": "1.12.1", @@ -8857,10 +21539,16 @@ "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", "dev": true }, + "undici": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-4.16.0.tgz", + "integrity": "sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw==" + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, "requires": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -8896,6 +21584,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, "requires": { "has-value": "^0.3.1", "isobject": "^3.0.0" @@ -8905,6 +21594,7 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, "requires": { "get-value": "^2.0.3", "has-values": "^0.1.4", @@ -8915,6 +21605,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, "requires": { "isarray": "1.0.0" } @@ -8924,7 +21615,8 @@ "has-values": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true } } }, @@ -8935,50 +21627,50 @@ "dev": true }, "update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz", + "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==", "dev": true, "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", + "boxen": "^5.0.0", + "chalk": "^4.1.0", "configstore": "^5.0.1", "has-yarn": "^2.1.0", "import-lazy": "^2.1.0", "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", "semver-diff": "^3.1.1", "xdg-basedir": "^4.0.0" }, "dependencies": { - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "yallist": "^4.0.0" } }, - "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 - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "lru-cache": "^6.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, @@ -9000,7 +21692,8 @@ "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true }, "url": { "version": "0.10.3", @@ -9023,7 +21716,8 @@ "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true }, "util-deprecate": { "version": "1.0.2", @@ -9063,6 +21757,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, "requires": { "homedir-polyfill": "^1.0.1" } @@ -9077,11 +21772,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "validator": { - "version": "13.1.17", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.1.17.tgz", - "integrity": "sha512-zL5QBoemJ3jYFb2/j38y7ljhwYGXVLUp8H6W1nVxadnAOvUOytec+L7BHh1oBQ82/TzWXHd+GSaxUWp4lROkLg==" - }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", @@ -9199,6 +21889,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -9336,9 +22027,9 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, "workerpool": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", - "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", "dev": true }, "wrap-ansi": { @@ -9356,15 +22047,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -9384,17 +22066,17 @@ "dev": true }, "xlsx": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.0.tgz", - "integrity": "sha512-bZ36FSACiAyjoldey1+7it50PMlDp1pcAJrZKcVZHzKd8BC/z6TQ/QAN8onuqcepifqSznR6uKnjPhaGt6ig9A==", + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.17.3.tgz", + "integrity": "sha512-dGZKfyPSXfnoITruwisuDVZkvnxhjgqzWJXBJm2Khmh01wcw8//baRUvhroVRhW2SLbnlpGcCZZbeZO1qJgMIw==", "requires": { "adler-32": "~1.2.0", "cfb": "^1.1.4", - "codepage": "~1.14.0", + "codepage": "~1.15.0", "commander": "~2.17.1", "crc-32": "~1.2.0", "exit-on-epipe": "~1.0.1", - "fflate": "^0.3.8", + "fflate": "^0.7.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" @@ -9408,18 +22090,18 @@ } }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "xtend": { "version": "4.0.2", diff --git a/api/package.json b/api/package.json index 39c6e9c8a2..32093db960 100644 --- a/api/package.json +++ b/api/package.json @@ -1,7 +1,7 @@ { - "name": "biohubbc-api", + "name": "sims-api", "version": "0.0.0", - "description": "API for BioHubBC App", + "description": "API for SIMS Web App", "license": "Apache-2.0", "main": "app", "repository": { @@ -24,74 +24,77 @@ "format:fix": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss}\"" }, "engines": { - "node": ">= 10.0.0", + "node": ">= 14.0.0", "npm": ">= 6.0.0" }, "dependencies": { + "@elastic/elasticsearch": "~8.1.0", "adm-zip": "~0.5.5", - "ajv": "~8.6.2", + "ajv": "~8.6.3", "aws-sdk": "~2.742.0", "axios": "~0.21.4", + "clamdjs": "~1.0.2", "db-migrate": "~0.11.11", "db-migrate-pg": "~1.2.2", - "xlsx": "~0.17.0", - "clamdjs": "~1.0.2", "express": "~4.17.1", - "express-openapi": "~7.0.1", + "express-openapi": "~9.3.0", "jsonpath": "~1.1.1", "jsonwebtoken": "~8.5.1", - "jwks-rsa": "~1.9.0", - "knex": "~0.21.4", + "jwks-rsa": "~2.0.5", + "knex": "~1.0.1", "lodash": "~4.17.21", "mime": "~2.5.2", - "moment": "~2.29.1", - "multer": "^1.4.2", - "pg": "~8.5.1", - "qs": "~6.9.4", + "moment": "~2.29.2", + "multer": "~1.4.3", + "pg": "~8.7.1", + "qs": "~6.10.1", "sql-template-strings": "~2.2.2", - "swagger-object-validator": "~1.2.2", - "typescript": "~3.9.4", + "swagger-ui-express": "~4.3.0", + "typescript": "~4.1.6", "uuid": "~8.3.2", - "validator": "~13.1.1", - "winston": "~3.3.3" + "winston": "~3.3.3", + "xlsx": "~0.17.0", + "xml2js": "~0.4.23" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "~1.0.1", "@types/adm-zip": "~0.4.34", - "@types/chai": "~4.2.12", - "@types/express": "~4.17.0", - "@types/geojson": "~7946.0.3", - "@types/gulp": "~4.0.6", + "@types/chai": "~4.2.22", + "@types/express": "~4.17.13", + "@types/geojson": "~7946.0.8", + "@types/gulp": "~4.0.9", "@types/jsonpath": "~0.2.0", - "@types/jsonwebtoken": "~8.5.0", - "@types/lodash": "~4.14.173", + "@types/jsonwebtoken": "~8.5.5", + "@types/lodash": "~4.14.176", "@types/mime": "~2.0.3", - "@types/mocha": "~8.0.1", - "@types/multer": "^1.4.5", - "@types/pg": "~7.14.4", - "@types/sinon": "^10.0.0", - "@types/sinon-chai": "^3.2.5", - "@types/uuid": "~8.3.0", + "@types/mocha": "~9.0.0", + "@types/multer": "~1.4.7", + "@types/node": "~14.14.31", + "@types/pg": "~8.6.1", + "@types/sinon": "~10.0.4", + "@types/sinon-chai": "~3.2.5", + "@types/swagger-ui-express": "~4.1.3", + "@types/uuid": "~8.3.1", + "@types/xml2js": "^0.4.9", "@types/yamljs": "~0.2.31", - "@typescript-eslint/eslint-plugin": "~3.7.1", - "@typescript-eslint/parser": "~3.7.1", - "chai": "~4.2.0", + "@typescript-eslint/eslint-plugin": "~4.33.0", + "@typescript-eslint/parser": "~4.33.0", + "chai": "~4.3.4", "del": "~6.0.0", - "eslint": "~7.5.0", - "eslint-config-prettier": "~6.11.0", - "eslint-plugin-prettier": "~3.1.4", + "eslint": "~7.32.0", + "eslint-config-prettier": "~6.15.0", + "eslint-plugin-prettier": "~3.3.1", "gulp": "~4.0.2", "gulp-typescript": "~5.0.1", - "mocha": "~8.2.1", - "nock": "~13.0.3", - "nodemon": "~2.0.7", + "mocha": "~8.4.0", + "nodemon": "~2.0.14", "npm-run-all": "~4.1.5", "nyc": "~15.1.0", "prettier": "~2.2.1", - "sinon": "^10.0.0", - "sinon-chai": "^3.6.0", - "supertest": "~6.0.1", + "prettier-plugin-organize-imports": "~2.3.4", + "sinon": "~11.1.2", + "sinon-chai": "~3.7.0", "ts-mocha": "~8.0.0", - "ts-node": "~9.1.1" + "ts-node": "~10.4.0" } } diff --git a/api/src/__mocks__/db.ts b/api/src/__mocks__/db.ts index 0b46c6de24..a758f95324 100644 --- a/api/src/__mocks__/db.ts +++ b/api/src/__mocks__/db.ts @@ -1,3 +1,6 @@ +import { Request, Response } from 'express'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; import { IDBConnection } from '../database/db'; /** @@ -24,8 +27,55 @@ export const getMockDBConnection = (config?: Partial): IDBConnect // do nothing }, query: async () => { - // do nothing + return (undefined as unknown) as QueryResult; }, ...config }; }; + +export type ExtendedMockReq = MockReq & Request; +export class MockReq { + query = {}; + params = {}; + body = {}; + files: any[] = []; +} + +export type ExtendedMockRes = MockRes & Response; +export class MockRes { + statusValue: any; + status = sinon.fake((value: any) => { + this.statusValue = value; + + return this; + }); + + jsonValue: any; + json = sinon.fake((value: any) => { + this.jsonValue = value; + + return this; + }); + + sendValue: any; + send = sinon.fake((value: any) => { + this.sendValue = value; + + return this; + }); +} + +/** + * Returns several mocks for testing RequestHandler responses. + * + * @return {*} + */ +export const getRequestHandlerMocks = () => { + const mockReq = new MockReq() as ExtendedMockReq; + + const mockRes = new MockRes() as ExtendedMockRes; + + const mockNext = sinon.fake(); + + return { mockReq, mockRes, mockNext }; +}; diff --git a/api/src/app.ts b/api/src/app.ts index 5afac6bf73..36088a5f35 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -1,11 +1,12 @@ -import express from 'express'; +import express, { NextFunction, Request, Response } from 'express'; import { initialize } from 'express-openapi'; import multer from 'multer'; -import { OpenAPI } from 'openapi-types'; -import { initDBPool, defaultPoolConfig } from './database/db'; -import { ensureCustomError } from './errors/CustomError'; +import { OpenAPIV3 } from 'openapi-types'; +import swaggerUIExperss from 'swagger-ui-express'; +import { defaultPoolConfig, initDBPool } from './database/db'; +import { ensureHTTPError, HTTPErrorType } from './errors/custom-error'; import { rootAPIDoc } from './openapi/root-api-doc'; -import { authenticate, authorize } from './security/auth-utils'; +import { authenticateRequest } from './request-handlers/security/authentication'; import { getLogger } from './utils/logger'; const defaultLog = getLogger('app'); @@ -24,8 +25,8 @@ const MAX_UPLOAD_FILE_SIZE = Number(process.env.MAX_UPLOAD_FILE_SIZE) || 5242880 const app: express.Express = express(); // Enable CORS -app.use(function (req: any, res: any, next: any) { - defaultLog.info(`${req.method} ${req.url}`); +app.use(function (req: Request, res: Response, next: NextFunction) { + defaultLog.info({ label: 'req', message: `${req.method} ${req.url}` }); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Authorization, responseType'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE, HEAD'); @@ -36,26 +37,47 @@ app.use(function (req: any, res: any, next: any) { }); // Initialize express-openapi framework -initialize({ - apiDoc: rootAPIDoc as OpenAPI.Document, // base open api spec +const openAPIFramework = initialize({ + apiDoc: { + ...(rootAPIDoc as OpenAPIV3.Document), // base open api spec + 'x-express-openapi-additional-middleware': [validateAllResponses], + 'x-express-openapi-validation-strict': true + }, app: app, // express app to initialize paths: './src/paths', // base folder for endpoint routes pathsIgnore: new RegExp('.(spec|test)$'), // ignore test files in paths routesGlob: '**/*.{ts,js}', // updated default to allow .ts routesIndexFileRegExp: /(?:index)?\.[tj]s$/, // updated default to allow .ts promiseMode: true, // allow endpoint handlers to return promises + docsPath: '/raw-api-docs', // path to view raw openapi spec consumesMiddleware: { 'application/json': express.json({ limit: MAX_REQ_BODY_SIZE }), - 'multipart/form-data': multer({ - storage: multer.memoryStorage(), - limits: { fileSize: MAX_UPLOAD_FILE_SIZE } - }).array('media', MAX_UPLOAD_NUM_FILES), + 'multipart/form-data': function (req, res, next) { + const multerRequestHandler = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_UPLOAD_FILE_SIZE } + }).array('media', MAX_UPLOAD_NUM_FILES); + + multerRequestHandler(req, res, (error?: any) => { + if (error) { + return next(error); + } + + if (req.files && req.files.length) { + // Set original request file field to empty string to satisfy OpenAPI validation + // See: https://www.npmjs.com/package/express-openapi#argsconsumesmiddleware + (req.files as Express.Multer.File[]).forEach((file) => (req.body[file.fieldname] = '')); + } + + return next(); + }); + }, 'application/x-www-form-urlencoded': express.urlencoded({ limit: MAX_REQ_BODY_SIZE, extended: true }) }, securityHandlers: { - // applies authentication logic - Bearer: async function (req: any, scopes: string[]) { - return (await authenticate(req)) && authorize(req, scopes); + // authenticates the request bearer token, for endpoints that specify `Bearer` security + Bearer: async function (req: any) { + return authenticateRequest(req); } }, errorTransformer: function (openapiError: object, ajvError: object): object { @@ -63,16 +85,21 @@ initialize({ defaultLog.error({ label: 'errorTransformer', message: 'ajvError', ajvError }); return ajvError; }, - // If `next` is not inclduded express will silently skip calling the `errorMiddleware` entirely. + // If `next` is not included express will silently skip calling the `errorMiddleware` entirely. // eslint-disable-next-line @typescript-eslint/no-unused-vars errorMiddleware: function (error, req, res, next) { // Ensure all errors (intentionally thrown or not) are in the same format as specified by the schema - const httpError = ensureCustomError(error); + const httpError = ensureHTTPError(error); - res.status(httpError.status).json(httpError); + res + .status(httpError.status) + .json({ name: httpError.name, status: httpError.status, message: httpError.message, errors: httpError.errors }); } }); +// Path to view beautified openapi spec +app.use('/api-docs', swaggerUIExperss.serve, swaggerUIExperss.setup(openAPIFramework.apiDoc)); + // Start api try { initDBPool(defaultPoolConfig); @@ -84,3 +111,60 @@ try { defaultLog.error({ label: 'start api', message: 'error', error }); process.exit(1); } + +/** + * Middleware to apply openapi response validation to all routes. + * + * Note: validates `` sent via `res.status().json()` against the matching openapi response schema + * for ``. + * + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ +function validateAllResponses(req: Request, res: Response, next: NextFunction) { + const isStrictValidation = !!req['apiDoc']['x-express-openapi-validation-strict'] || false; + + if (typeof res['validateResponse'] === 'function') { + const json = res.json; + + res.json = (...args) => { + if (res.get('x-express-openapi-validation-error-for')) { + // Already validated, return + return json.apply(res, args); + } + + const body = args[0]; + + const validationResult: { message: any; errors: any[] } | undefined = res['validateResponse']( + res.statusCode, + body + ); + + let validationMessage = ''; + let errorList = []; + + if (validationResult?.errors) { + validationMessage = `Invalid response for status code ${res.statusCode}`; + + errorList = Array.from(validationResult.errors); + + // Set to avoid a loop, and to provide the original status code + res.set('x-express-openapi-validation-error-for', res.statusCode.toString()); + } + + if (!isStrictValidation || !validationResult?.errors) { + return json.apply(res, args); + } else { + return res.status(500).json({ + name: HTTPErrorType.INTERNAL_SERVER_ERROR, + status: 500, + message: validationMessage, + errors: errorList + }); + } + }; + } + + next(); +} diff --git a/api/src/constants/attachments.ts b/api/src/constants/attachments.ts new file mode 100644 index 0000000000..7c47db4811 --- /dev/null +++ b/api/src/constants/attachments.ts @@ -0,0 +1,4 @@ +export enum ATTACHMENT_TYPE { + REPORT = 'Report', + OTHER = 'Other' +} diff --git a/api/src/constants/codes.ts b/api/src/constants/codes.ts index d6e806695d..b48e19332d 100644 --- a/api/src/constants/codes.ts +++ b/api/src/constants/codes.ts @@ -25,136 +25,136 @@ export const coordinator_agency = [ { id: 24, name: 'BBA Engineering Ltd.' }, { id: 25, name: 'BC Conservation Foundation (BCCF)' }, { id: 26, name: 'BC Hydro' }, - { id: 27, name: 'BC Ministry of Environment & Climate Change Strategy' }, - { id: 28, name: 'BC Ministry of Forests, Lands, Natural Resource Operations & Rural Development' }, - { id: 29, name: 'BC Ministry of Transportation & Infrastructure' }, - { id: 30, name: 'BCIT Fish Wildlife and Recreation Program' }, - { id: 31, name: 'Burt and Associates' }, - { id: 32, name: 'Canadian Forest Products' }, - { id: 33, name: 'Capital Regional District (CRD)' }, - { id: 34, name: 'Cariboo Environmental Quality Consulting Ltd.' }, - { id: 35, name: 'Cariboo Envirotech Ltd.' }, - { id: 36, name: 'Carleton University' }, - { id: 37, name: 'Cascade Environmental Resource Group Ltd.' }, - { id: 38, name: 'Castus Environmental Services Ltd.' }, - { id: 39, name: 'Central Westcoast Forest Society' }, - { id: 40, name: 'Chu Cho Environmental' }, - { id: 41, name: 'City of Abbotsford' }, - { id: 42, name: 'City of Chilliwack' }, - { id: 43, name: 'City of Coquitlam' }, - { id: 44, name: 'City of Kelowna Environment Division' }, - { id: 45, name: 'Clark University' }, - { id: 46, name: 'Clearwater Environmental Consulting' }, - { id: 47, name: 'Coast River Environmental Services Ltd.' }, - { id: 48, name: 'Coldstream Ecology Ltd.' }, - { id: 49, name: 'College of New Caledonia' }, - { id: 50, name: 'Columbia Environmental Consulting Ltd.' }, - { id: 51, name: 'Columbia Power Corporation' }, - { id: 52, name: 'Colville Confederated Tribes Fish and Wildlife' }, - { id: 53, name: 'Comox Valley Project Watershed Society' }, - { id: 54, name: 'Cooper Beauchesne and Associates Ltd.' }, - { id: 55, name: 'Copcan Contracting Ltd.' }, - { id: 56, name: 'Cornice Environmental Consulting Ltd.' }, - { id: 57, name: 'Corvidae Environmental Consulting Inc.' }, - { id: 58, name: 'Crane Creek Enterprises' }, - { id: 59, name: 'CSR Environmetnal Ltd.' }, - { id: 60, name: 'Current Environmental Ltd.' }, - { id: 61, name: 'D. Burt and Associates' }, - { id: 62, name: 'D.R. Clough Consulting' }, - { id: 63, name: 'David Bustard and Associates Ltd.' }, - { id: 64, name: 'Davis Environmental Ltd.' }, - { id: 65, name: 'Dayesi Services' }, - { id: 66, name: 'Department of Fisheries and Oceans (DFO)' }, - { id: 67, name: 'DGR Consulting' }, - { id: 68, name: 'Dillon Consulting Limited' }, - { id: 69, name: 'District of Kent' }, - { id: 70, name: 'District of North Vancouver' }, - { id: 71, name: 'District of Saanich' }, - { id: 72, name: 'Diversified Environmental Services Ltd.' }, - { id: 73, name: 'DWB Consulting Services Ltd.' }, - { id: 74, name: 'East Carolina University - Department of Biology' }, - { id: 75, name: 'EBB Environmental Consulting Inc.' }, - { id: 76, name: 'Ecofish Research Ltd.' }, - { id: 77, name: 'Ecofor Consulting' }, - { id: 78, name: 'Ecologic Consulting Ltd.' }, - { id: 79, name: 'EcoMetrix Incorporated' }, - { id: 80, name: 'Ecora Engineering and Resource Group Ltd.' }, - { id: 81, name: 'Ecoscape Environmental Consultants Ltd.' }, - { id: 82, name: 'Eco-Web Ecological Consulting Ltd.' }, - { id: 83, name: 'EDI Environmental Dynamics Inc.' }, - { id: 84, name: 'Elevate Environmental Inc.' }, - { id: 85, name: 'Elk River Alliance' }, - { id: 86, name: 'ENKON Environmental Ltd.' }, - { id: 87, name: 'Envirologic Consulting Inc.' }, - { id: 88, name: 'Environment and Climate Change Canada' }, - { id: 89, name: 'Envirowest Consultants Inc.' }, - { id: 90, name: 'ERM Consultants Canada Ltd.' }, - { id: 91, name: 'Esther Guimond Consulting' }, - { id: 92, name: 'Estsek Environmental Services LLP' }, - { id: 93, name: 'EXP Services Inc.' }, - { id: 94, name: 'FINS Consulting Ltd.' }, - { id: 95, name: 'Fish-Kissing Weasels Environmental' }, - { id: 96, name: 'ForFish Consulting' }, - { id: 97, name: 'Forsite Consultants Ltd.' }, - { id: 98, name: 'FortisBC' }, - { id: 99, name: 'Fraser Valley Watersheds Coalition' }, - { id: 100, name: 'Freshwater Fisheries Society of BC' }, - { id: 101, name: 'FSCI Biological Concultants' }, - { id: 102, name: 'G3 Consulting Ltd.' }, - { id: 103, name: 'GeoMarine Environmental Consultants Ltd.' }, - { id: 104, name: 'GG Oliver and Associates Environmental Science' }, - { id: 105, name: 'Gitanyow Fisheries Authority' }, - { id: 106, name: 'Gitksan Watershed Authorities' }, - { id: 107, name: 'Golder Associates Ltd.' }, - { id: 108, name: 'Grassroots Environmental Services' }, - { id: 109, name: 'Hatfield Consulting Ltd.' }, - { id: 110, name: 'HCR Environmental Consulting' }, - { id: 111, name: 'Hemmera' }, - { id: 112, name: 'High Country Consulting' }, - { id: 113, name: 'Hill Environmental Ltd.' }, - { id: 114, name: 'Hocquard Consulting' }, - { id: 115, name: 'I.C. Ramsay and Associates' }, - { id: 116, name: 'Ingersol Mountain Enterprise Ltd.' }, - { id: 117, name: 'Inland Timber Management Ltd.' }, - { id: 118, name: 'InStream Fisheries Research Inc.' }, - { id: 119, name: 'Interior Reforestation Co. Ltd.' }, - { id: 120, name: 'IRC Integrated Resource Consultants Inc.' }, - { id: 121, name: 'ISL Engineering and Land Services Ltd.' }, - { id: 122, name: 'Iverson & MacKenzie Biological Consulting Ltd.' }, - { id: 123, name: 'Jacobs Canada Inc.' }, - { id: 124, name: 'JBL Environmental Services Ltd.' }, - { id: 125, name: 'Karen L Grainger' }, - { id: 126, name: 'Kawa Enginnering Ltd.' }, - { id: 127, name: 'Kerr Wood Leidal Associates Ltd.' }, - { id: 128, name: 'Keystone Environmental Ltd.' }, - { id: 129, name: 'Klohn Crippen Berger Ltd.' }, - { id: 130, name: 'Knight Piesold Ltd.' }, - { id: 131, name: 'Ktunaxa Nation Council' }, - { id: 132, name: 'Lake Trail Environmental Consulting' }, - { id: 133, name: 'Letts Environmental Consultants Ltd.' }, - { id: 134, name: 'LGL Limited' }, - { id: 135, name: 'Lheidli T Enneh First Nations' }, - { id: 136, name: 'Limnotek Research and Development Inc.' }, - { id: 137, name: 'Living Resources Environmental' }, - { id: 138, name: 'Lorax Environmental Services Ltd.' }, - { id: 139, name: 'Lotic Environmental Ltd.' }, - { id: 140, name: 'M.C. Wright and Associates Ltd.' }, - { id: 141, name: 'Madrone Environmental Services Ltd.' }, - { id: 142, name: 'Mainstream Aquatics Ltd.' }, - { id: 143, name: 'Mainstream Biological Consulting Inc.' }, - { id: 144, name: 'Marine Harvest Canada' }, - { id: 145, name: 'Marlim Ecological Consulting Ltd.' }, - { id: 146, name: 'Masse Environmental Consultants Ltd.' }, - { id: 147, name: 'Matrix Solutions Inc.' }, - { id: 148, name: 'Max Planck Institute, Ploen, Germany' }, - { id: 149, name: 'McCleary Aquatic Systems Consulting' }, - { id: 150, name: 'McElhanney Ltd.' }, - { id: 151, name: 'McGill University, Redpath Museum' }, - { id: 152, name: 'McTavish Resource & Management Consultants Ltd.' }, - { id: 153, name: 'Metro Vancouver (GVRD)' }, - { id: 154, name: 'Metro Vancouver Regional Parks' }, - { id: 155, name: 'Michigan State University' }, - { id: 156, name: 'Mid Vancouver Island Habitat Enhancement Society' }, + { id: 27, name: 'BCIT Fish Wildlife and Recreation Program' }, + { id: 28, name: 'Burt and Associates' }, + { id: 29, name: 'Canadian Forest Products' }, + { id: 30, name: 'Capital Regional District (CRD)' }, + { id: 31, name: 'Cariboo Environmental Quality Consulting Ltd.' }, + { id: 32, name: 'Cariboo Envirotech Ltd.' }, + { id: 33, name: 'Carleton University' }, + { id: 34, name: 'Cascade Environmental Resource Group Ltd.' }, + { id: 35, name: 'Castus Environmental Services Ltd.' }, + { id: 36, name: 'Central Westcoast Forest Society' }, + { id: 37, name: 'Chu Cho Environmental' }, + { id: 38, name: 'City of Abbotsford' }, + { id: 39, name: 'City of Chilliwack' }, + { id: 40, name: 'City of Coquitlam' }, + { id: 41, name: 'City of Kelowna Environment Division' }, + { id: 42, name: 'Clark University' }, + { id: 43, name: 'Clearwater Environmental Consulting' }, + { id: 44, name: 'Coast River Environmental Services Ltd.' }, + { id: 45, name: 'Coldstream Ecology Ltd.' }, + { id: 46, name: 'College of New Caledonia' }, + { id: 47, name: 'Columbia Environmental Consulting Ltd.' }, + { id: 48, name: 'Columbia Power Corporation' }, + { id: 49, name: 'Colville Confederated Tribes Fish and Wildlife' }, + { id: 50, name: 'Comox Valley Project Watershed Society' }, + { id: 51, name: 'Cooper Beauchesne and Associates Ltd.' }, + { id: 52, name: 'Copcan Contracting Ltd.' }, + { id: 53, name: 'Cornice Environmental Consulting Ltd.' }, + { id: 54, name: 'Corvidae Environmental Consulting Inc.' }, + { id: 55, name: 'Crane Creek Enterprises' }, + { id: 56, name: 'CSR Environmental Ltd.' }, + { id: 57, name: 'Current Environmental Ltd.' }, + { id: 58, name: 'D. Burt and Associates' }, + { id: 59, name: 'D.R. Clough Consulting' }, + { id: 60, name: 'David Bustard and Associates Ltd.' }, + { id: 61, name: 'Davis Environmental Ltd.' }, + { id: 62, name: 'Dayesi Services' }, + { id: 63, name: 'Department of Fisheries and Oceans (DFO)' }, + { id: 64, name: 'DGR Consulting' }, + { id: 65, name: 'Dillon Consulting Limited' }, + { id: 66, name: 'District of Kent' }, + { id: 67, name: 'District of North Vancouver' }, + { id: 68, name: 'District of Saanich' }, + { id: 69, name: 'Diversified Environmental Services Ltd.' }, + { id: 70, name: 'DWB Consulting Services Ltd.' }, + { id: 71, name: 'East Carolina University - Department of Biology' }, + { id: 72, name: 'EBB Environmental Consulting Inc.' }, + { id: 73, name: 'Ecofish Research Ltd.' }, + { id: 74, name: 'Ecofor Consulting' }, + { id: 75, name: 'Ecologic Consulting Ltd.' }, + { id: 76, name: 'EcoMetrix Incorporated' }, + { id: 77, name: 'Ecora Engineering and Resource Group Ltd.' }, + { id: 78, name: 'Ecoscape Environmental Consultants Ltd.' }, + { id: 79, name: 'Eco-Web Ecological Consulting Ltd.' }, + { id: 80, name: 'EDI Environmental Dynamics Inc.' }, + { id: 81, name: 'Elevate Environmental Inc.' }, + { id: 82, name: 'Elk River Alliance' }, + { id: 83, name: 'ENKON Environmental Ltd.' }, + { id: 84, name: 'Envirologic Consulting Inc.' }, + { id: 85, name: 'Environment and Climate Change Canada' }, + { id: 86, name: 'Envirowest Consultants Inc.' }, + { id: 87, name: 'ERM Consultants Canada Ltd.' }, + { id: 88, name: 'Esther Guimond Consulting' }, + { id: 89, name: 'Estsek Environmental Services LLP' }, + { id: 90, name: 'EXP Services Inc.' }, + { id: 91, name: 'FINS Consulting Ltd.' }, + { id: 92, name: 'Fish-Kissing Weasels Environmental' }, + { id: 93, name: 'ForFish Consulting' }, + { id: 94, name: 'Forsite Consultants Ltd.' }, + { id: 95, name: 'FortisBC' }, + { id: 96, name: 'Fraser Valley Watersheds Coalition' }, + { id: 97, name: 'Freshwater Fisheries Society of BC' }, + { id: 98, name: 'FSCI Biological Concultants' }, + { id: 99, name: 'G3 Consulting Ltd.' }, + { id: 100, name: 'GeoMarine Environmental Consultants Ltd.' }, + { id: 101, name: 'GG Oliver and Associates Environmental Science' }, + { id: 102, name: 'Gitanyow Fisheries Authority' }, + { id: 103, name: 'Gitksan Watershed Authorities' }, + { id: 104, name: 'Golder Associates Ltd.' }, + { id: 105, name: 'Grassroots Environmental Services' }, + { id: 106, name: 'Hatfield Consulting Ltd.' }, + { id: 107, name: 'HCR Environmental Consulting' }, + { id: 108, name: 'Hemmera' }, + { id: 109, name: 'High Country Consulting' }, + { id: 110, name: 'Hill Environmental Ltd.' }, + { id: 111, name: 'Hocquard Consulting' }, + { id: 112, name: 'I.C. Ramsay and Associates' }, + { id: 113, name: 'Ingersol Mountain Enterprise Ltd.' }, + { id: 114, name: 'Inland Timber Management Ltd.' }, + { id: 115, name: 'InStream Fisheries Research Inc.' }, + { id: 116, name: 'Interior Reforestation Co. Ltd.' }, + { id: 117, name: 'IRC Integrated Resource Consultants Inc.' }, + { id: 118, name: 'ISL Engineering and Land Services Ltd.' }, + { id: 119, name: 'Iverson & MacKenzie Biological Consulting Ltd.' }, + { id: 120, name: 'Jacobs Canada Inc.' }, + { id: 121, name: 'JBL Environmental Services Ltd.' }, + { id: 122, name: 'Karen L Grainger' }, + { id: 123, name: 'Kawa Enginnering Ltd.' }, + { id: 124, name: 'Kerr Wood Leidal Associates Ltd.' }, + { id: 125, name: 'Keystone Environmental Ltd.' }, + { id: 126, name: 'Klohn Crippen Berger Ltd.' }, + { id: 127, name: 'Knight Piesold Ltd.' }, + { id: 128, name: 'Ktunaxa Nation Council' }, + { id: 129, name: 'Lake Trail Environmental Consulting' }, + { id: 130, name: 'Letts Environmental Consultants Ltd.' }, + { id: 131, name: 'LGL Limited' }, + { id: 132, name: 'Lheidli T Enneh First Nations' }, + { id: 133, name: 'Limnotek Research and Development Inc.' }, + { id: 134, name: 'Living Resources Environmental' }, + { id: 135, name: 'Lorax Environmental Services Ltd.' }, + { id: 136, name: 'Lotic Environmental Ltd.' }, + { id: 137, name: 'M.C. Wright and Associates Ltd.' }, + { id: 138, name: 'Madrone Environmental Services Ltd.' }, + { id: 139, name: 'Mainstream Aquatics Ltd.' }, + { id: 140, name: 'Mainstream Biological Consulting Inc.' }, + { id: 141, name: 'Marine Harvest Canada' }, + { id: 142, name: 'Marlim Ecological Consulting Ltd.' }, + { id: 143, name: 'Masse Environmental Consultants Ltd.' }, + { id: 144, name: 'Matrix Solutions Inc.' }, + { id: 145, name: 'Max Planck Institute, Ploen, Germany' }, + { id: 146, name: 'McCleary Aquatic Systems Consulting' }, + { id: 147, name: 'McElhanney Ltd.' }, + { id: 148, name: 'McGill University, Redpath Museum' }, + { id: 149, name: 'McTavish Resource & Management Consultants Ltd.' }, + { id: 150, name: 'Metro Vancouver (GVRD)' }, + { id: 151, name: 'Metro Vancouver Regional Parks' }, + { id: 152, name: 'Michigan State University' }, + { id: 153, name: 'Mid Vancouver Island Habitat Enhancement Society' }, + { id: 154, name: 'Ministry of Environment & Climate Change Strategy' }, + { id: 155, name: 'Ministry of Forests, Lands, Natural Resource Operations & Rural Development' }, + { id: 156, name: 'Ministry of Transportation & Infrastructure' }, { id: 157, name: 'Minnow Environmental Inc.' }, { id: 158, name: 'Montana Fish, Wildlife & Parks' }, { id: 159, name: 'Mount Polley Mining Corporation' }, diff --git a/api/src/constants/database.ts b/api/src/constants/database.ts index 2c7d1d11eb..e07bdd2a8a 100644 --- a/api/src/constants/database.ts +++ b/api/src/constants/database.ts @@ -1,3 +1,9 @@ +/** + * Identity sources supported/recognized by the database. + * + * @export + * @enum {number} + */ export enum SYSTEM_IDENTITY_SOURCE { DATABASE = 'DATABASE', IDIR = 'IDIR', diff --git a/api/src/constants/keycloak.ts b/api/src/constants/keycloak.ts new file mode 100644 index 0000000000..27490050f6 --- /dev/null +++ b/api/src/constants/keycloak.ts @@ -0,0 +1,4 @@ +// Possible identity sources for BCEID users +export const EXTERNAL_BCEID_IDENTITY_SOURCES = ['BCEID-BASIC-AND-BUSINESS', 'BCEID']; +// Possible identity sources for IDIR users +export const EXTERNAL_IDIR_IDENTITY_SOURCES = ['IDIR']; diff --git a/api/src/constants/notifications.ts b/api/src/constants/notifications.ts new file mode 100644 index 0000000000..0db0d8711d --- /dev/null +++ b/api/src/constants/notifications.ts @@ -0,0 +1,19 @@ +import { IgcNotifyGenericMessage } from '../models/gcnotify'; + +//admin email template for new access requests +export const ACCESS_REQUEST_ADMIN_EMAIL: IgcNotifyGenericMessage = { + subject: 'SIMS: A request for access has been received.', + header: 'A request for access to the Species Inventory Management System has been submitted.', + body1: `To review the request,`, + body2: 'This is an automated message from the BioHub Species Inventory Management System', + footer: '' +}; + +//admin email template for approval of access requests +export const ACCESS_REQUEST_APPROVAL_ADMIN_EMAIL: IgcNotifyGenericMessage = { + subject: 'SIMS: Your request for access has been approved.', + header: 'Your request for access to the Species Inventory Management System has been approved.', + body1: `To access the site, `, + body2: 'This is an automated message from the BioHub Species Inventory Management System', + footer: '' +}; diff --git a/api/src/constants/roles.ts b/api/src/constants/roles.ts index 8b4dad030e..85f927db18 100644 --- a/api/src/constants/roles.ts +++ b/api/src/constants/roles.ts @@ -1,12 +1,25 @@ /** - * System level roles + * System level roles. * * @export * @enum {number} */ export enum SYSTEM_ROLE { SYSTEM_ADMIN = 'System Administrator', - PROJECT_ADMIN = 'Project Administrator' + PROJECT_CREATOR = 'Creator', + DATA_ADMINISTRATOR = 'Data Administrator' +} + +/** + * Project level roles. + * + * @export + * @enum {number} + */ +export enum PROJECT_ROLE { + PROJECT_LEAD = 'Project Lead', + PROJECT_EDITOR = 'Editor', + PROJECT_VIEWER = 'Viewer' } /** diff --git a/api/src/database/db.test.ts b/api/src/database/db.test.ts index b84d5e4d27..ae219bcf14 100644 --- a/api/src/database/db.test.ts +++ b/api/src/database/db.test.ts @@ -3,7 +3,8 @@ import { describe } from 'mocha'; import * as pg from 'pg'; import Sinon from 'sinon'; import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; -import { setSystemUserContextSQL } from '../queries/user-context-queries'; +import { HTTPError } from '../errors/custom-error'; +import { setSystemUserContextSQL } from '../queries/database/user-context-queries'; import * as db from './db'; import { getAPIUserDBConnection, getDBConnection, getDBPool, IDBConnection, initDBPool } from './db'; @@ -36,7 +37,7 @@ describe('db', () => { expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Keycloak token is undefined'); + expect((actualError as HTTPError).message).to.equal('Keycloak token is undefined'); } }); @@ -118,7 +119,7 @@ describe('db', () => { expect.fail('Expected an error to be thrown'); } catch (error) { - expectedError = error; + expectedError = error as Error; } expect(expectedError.message).to.equal('DBPool is not initialized'); @@ -197,7 +198,7 @@ describe('db', () => { expect.fail('Expected an error to be thrown'); } catch (error) { - expectedError = error; + expectedError = error as Error; } expect(expectedError.message).to.equal('DBConnection is not open'); @@ -228,7 +229,7 @@ describe('db', () => { expect.fail('Expected an error to be thrown'); } catch (error) { - expectedError = error; + expectedError = error as Error; } expect(expectedError.message).to.equal('DBConnection is not open'); @@ -269,7 +270,7 @@ describe('db', () => { expect.fail('Expected an error to be thrown'); } catch (error) { - expectedError = error; + expectedError = error as Error; } expect(expectedError.message).to.equal('DBConnection is not open'); @@ -280,6 +281,10 @@ describe('db', () => { }); describe('getAPIUserDBConnection', () => { + afterEach(() => { + Sinon.restore(); + }); + it('calls getDBConnection for the biohub_api user', () => { const getDBConnectionStub = Sinon.stub(db, 'getDBConnection').returns( ('stubbed DBConnection object' as unknown) as IDBConnection @@ -287,7 +292,9 @@ describe('db', () => { getAPIUserDBConnection(); - expect(getDBConnectionStub).to.have.been.calledWith({ preferred_username: 'biohub_api@database' }); + expect(getDBConnectionStub).to.have.been.calledWith({ + preferred_username: 'biohub_api@database' + }); }); }); }); diff --git a/api/src/database/db.ts b/api/src/database/db.ts index 798fe840d1..9fc484969f 100644 --- a/api/src/database/db.ts +++ b/api/src/database/db.ts @@ -1,6 +1,6 @@ import * as pg from 'pg'; -import { HTTP400, HTTP500 } from '../errors/CustomError'; -import { setSystemUserContextSQL } from '../queries/user-context-queries'; +import { ApiExecuteSQLError, ApiGeneralError } from '../errors/custom-error'; +import { queries } from '../queries/queries'; import { getUserIdentifier, getUserIdentitySource } from '../utils/keycloak-utils'; import { getLogger } from '../utils/logger'; @@ -110,11 +110,11 @@ export interface IDBConnection { * * @param {string} text SQL text * @param {any[]} [values] SQL values array (optional) - * @return {*} {(Promise | void>)} + * @return {*} {(Promise>)} * @throws If the connection is not open. * @memberof IDBConnection */ - query: (text: string, values?: any[]) => Promise | void>; + query: (text: string, values?: any[]) => Promise>; /** * Get the ID of the system user in context. * @@ -237,17 +237,21 @@ export const getDBConnection = function (keycloakToken: object): IDBConnection { /** * Performs a query against this connection, returning the results. * + * @template T * @param {string} text SQL text * @param {any[]} [values] SQL values array (optional) * @throws {Error} if the connection is not open - * @return {*} {(Promise | void>)} + * @return {*} {Promise>} */ - const _query = async (text: string, values?: any[]): Promise | void> => { + const _query = async ( + text: string, + values?: any[] + ): Promise> => { if (!_client || !_isOpen) { throw Error('DBConnection is not open'); } - return _client.query(text, values || []); + return _client.query(text, values || []); }; const _getSystemUserID = () => { @@ -264,14 +268,17 @@ export const getDBConnection = function (keycloakToken: object): IDBConnection { const userIdentitySource = getUserIdentitySource(_token); if (!userIdentifier || !userIdentitySource) { - throw new HTTP400('Failed to identify authenticated user'); + throw new ApiGeneralError('Failed to identify authenticated user'); } // Set the user context for all queries made using this connection - const setSystemUserContextSQLStatement = setSystemUserContextSQL(userIdentifier, userIdentitySource); + const setSystemUserContextSQLStatement = queries.database.setSystemUserContextSQL( + userIdentifier, + userIdentitySource + ); if (!setSystemUserContextSQLStatement) { - throw new HTTP400('Failed to build SQL user context statement'); + throw new ApiExecuteSQLError('Failed to build SQL user context statement'); } try { @@ -282,7 +289,7 @@ export const getDBConnection = function (keycloakToken: object): IDBConnection { _systemUserId = response?.rows?.[0].api_set_context; } catch (error) { - throw new HTTP500('Failed to set user context', [error]); + throw new ApiExecuteSQLError('Failed to set user context', [error as object]); } }; diff --git a/api/src/errors/CustomError.test.ts b/api/src/errors/CustomError.test.ts deleted file mode 100644 index 6b06818e11..0000000000 --- a/api/src/errors/CustomError.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { CustomError, ensureCustomError, HTTP400, HTTP401, HTTP403, HTTP409, HTTP500 } from './CustomError'; - -describe('CustomError', () => { - describe('No error value provided', () => { - let message: string; - - before(() => { - message = 'response message'; - }); - - it('sets status code 400', function () { - expect(new HTTP400(message).status).to.equal(400); - }); - - it('sets status code 401', function () { - expect(new HTTP401(message).status).to.equal(401); - }); - - it('sets status code 403', function () { - expect(new HTTP403(message).status).to.equal(403); - }); - - it('sets status code 409', function () { - expect(new HTTP409(message).status).to.equal(409); - }); - - it('sets status code 500', function () { - expect(new HTTP500(message).status).to.equal(500); - }); - }); -}); - -describe('ensureCustomError', () => { - it('returns the original CustomError when a CustomError provided', function () { - const customError = new HTTP400('a custom error'); - - const ensuredError = ensureCustomError(customError); - - expect(ensuredError).to.be.instanceof(CustomError); - - expect(ensuredError).to.deep.equal(customError); - }); - - it('returns a CustomError when a non custom Error provided', function () { - const nonCustomError = new Error('a non custom error'); - - const ensuredError = ensureCustomError(nonCustomError); - - expect(ensuredError).to.be.instanceof(CustomError); - - expect(ensuredError.status).to.equal(500); - expect(ensuredError.message).to.equal('a non custom error'); - }); -}); diff --git a/api/src/errors/CustomError.ts b/api/src/errors/CustomError.ts deleted file mode 100644 index daffae5cc1..0000000000 --- a/api/src/errors/CustomError.ts +++ /dev/null @@ -1,60 +0,0 @@ -export class CustomError implements Error { - name: string; - status: number; - message: string; - errors?: (string | object)[]; - - constructor(name: string, status: number, message: string, errors?: (string | object)[]) { - this.name = name; - this.status = status; - this.message = message; - this.errors = errors || []; - } -} - -export class HTTP400 extends CustomError { - constructor(message: string, errors?: (string | object)[]) { - super('Bad Request', 400, message, errors); - } -} - -export class HTTP401 extends CustomError { - constructor(message: string, errors?: (string | object)[]) { - super('Unauthorized', 401, message, errors); - } -} - -export class HTTP403 extends CustomError { - constructor(message: string, errors?: (string | object)[]) { - super('Forbidden', 403, message, errors); - } -} - -export class HTTP409 extends CustomError { - constructor(message: string, errors?: (string | object)[]) { - super('Conflict', 409, message, errors); - } -} - -export class HTTP500 extends CustomError { - constructor(message: string, errors?: (string | object)[]) { - super('Internal Server Error', 500, message, errors); - } -} - -/** - * Ensures that the error is a `CustomError`. - * If `error` is a `CustomError`, then change nothing and return it. - * If `error` is not a `CustomError`, wrap it into an `HTTP500` error. - * - * @param {Error} error - * @param {string} [message] - * @return {*} - */ -export const ensureCustomError = (error: Error) => { - if (error instanceof CustomError) { - return error; - } - - return new HTTP500(error.message || error.name, [error.stack || '']); -}; diff --git a/api/src/errors/custom-error.test.ts b/api/src/errors/custom-error.test.ts new file mode 100644 index 0000000000..94d88508f9 --- /dev/null +++ b/api/src/errors/custom-error.test.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { describe } from 'mocha'; +import { DatabaseError } from 'pg'; +import { + ApiBuildSQLError, + ApiError, + ApiErrorType, + ApiExecuteSQLError, + ApiGeneralError, + ApiUnknownError, + ensureHTTPError, + HTTP400, + HTTP401, + HTTP403, + HTTP409, + HTTP500, + HTTPError +} from './custom-error'; + +describe('ApiError', () => { + describe('No error value provided', () => { + let message: string; + + before(() => { + message = 'response message'; + }); + + it('Creates Api General error', function () { + expect(new ApiGeneralError(message).name).to.equal(ApiErrorType.GENERAL); + }); + + it('Creates Api Unknown error', function () { + expect(new ApiUnknownError(message).name).to.equal(ApiErrorType.UNKNOWN); + }); + + it('Creates Api build SQL error', function () { + expect(new ApiBuildSQLError(message).name).to.equal(ApiErrorType.BUILD_SQL); + }); + + it('Creates Api execute SQL error', function () { + expect(new ApiExecuteSQLError(message).name).to.equal(ApiErrorType.EXECUTE_SQL); + }); + }); +}); + +describe('HTTPError', () => { + describe('No error value provided', () => { + let message: string; + + before(() => { + message = 'response message'; + }); + + it('sets status code 400', function () { + expect(new HTTP400(message).status).to.equal(400); + }); + + it('sets status code 401', function () { + expect(new HTTP401(message).status).to.equal(401); + }); + + it('sets status code 403', function () { + expect(new HTTP403(message).status).to.equal(403); + }); + + it('sets status code 409', function () { + expect(new HTTP409(message).status).to.equal(409); + }); + + it('sets status code 500', function () { + expect(new HTTP500(message).status).to.equal(500); + }); + }); +}); + +describe('ensureHTTPError', () => { + it('returns the original HTTPError when a HTTPError provided', function () { + const httpError = new HTTP400('a http error'); + + const ensuredError = ensureHTTPError(httpError); + + expect(ensuredError).to.be.instanceof(HTTPError); + + expect(ensuredError).to.deep.equal(httpError); + }); + + it('returns a HTTPError when an ApiError provided', function () { + const apiError = new ApiError(ApiErrorType.UNKNOWN, 'an api error message'); + + const ensuredError = ensureHTTPError(apiError); + + expect(ensuredError).to.be.instanceof(HTTPError); + + expect(ensuredError.status).to.equal(500); + expect(ensuredError.message).to.equal('an api error message'); + }); + + it('returns a HTTPError when a DatabaseError provided', function () { + const databaseError = new DatabaseError('a db error message', 1, 'error'); + + const ensuredError = ensureHTTPError(databaseError); + + expect(ensuredError).to.be.instanceof(HTTPError); + + expect(ensuredError.status).to.equal(500); + expect(ensuredError.message).to.equal('Unexpected Database Error'); + expect(ensuredError.errors).to.eql([ + { + length: 1, + message: 'a db error message', + name: 'error' + } + ]); + }); + + it('returns a HTTPError when a non Http Error provided', function () { + const nonHttpError = new Error('a non http error'); + + const ensuredError = ensureHTTPError(nonHttpError); + + expect(ensuredError).to.be.instanceof(HTTPError); + + expect(ensuredError.status).to.equal(500); + expect(ensuredError.message).to.equal('Unexpected Error'); + expect(ensuredError.errors).to.eql(['Error', 'a non http error']); + }); + + it('returns a generic HTTPError when a non Error provided', function () { + const nonError = 'not an Error'; + + const ensuredError = ensureHTTPError(nonError); + + expect(ensuredError).to.be.instanceof(HTTPError); + + expect(ensuredError.status).to.equal(500); + expect(ensuredError.message).to.equal('Unexpected Error'); + expect(ensuredError.errors).to.eql([]); + }); +}); diff --git a/api/src/errors/custom-error.ts b/api/src/errors/custom-error.ts new file mode 100644 index 0000000000..4cb82552a5 --- /dev/null +++ b/api/src/errors/custom-error.ts @@ -0,0 +1,215 @@ +import { DatabaseError } from 'pg'; + +export enum ApiErrorType { + BUILD_SQL = 'Error constructing SQL query', + EXECUTE_SQL = 'Error executing SQL query', + GENERAL = 'Error', + UNKNOWN = 'Unknown Error' +} + +export class ApiError extends Error { + errors?: (string | object)[]; + + constructor(name: ApiErrorType, message: string, errors?: (string | object)[], stack?: string) { + super(message); + + this.name = name; + this.errors = errors || []; + this.stack = stack; + + if (stack) { + this.stack = stack; + } + + if (!this.stack) { + Error.captureStackTrace(this); + } + } +} + +/** + * Api encountered an error. + * + * @export + * @class ApiGeneralError + * @extends {ApiError} + */ +export class ApiGeneralError extends ApiError { + constructor(message: string, errors?: (string | object)[]) { + super(ApiErrorType.GENERAL, message, errors); + } +} + +/** + * API encountered an unknown/unexpected error. + * + * @export + * @class ApiUnknownError + * @extends {ApiError} + */ +export class ApiUnknownError extends ApiError { + constructor(message: string, errors?: (string | object)[]) { + super(ApiErrorType.UNKNOWN, message, errors); + } +} + +/** + * API failed to build SQL a query. + * + * @export + * @class ApiBuildSQLError + * @extends {ApiError} + */ +export class ApiBuildSQLError extends ApiError { + constructor(message: string, errors?: (string | object)[]) { + super(ApiErrorType.BUILD_SQL, message, errors); + } +} + +/** + * API executed a query against the database, but the response was missing data, or indicated the query failed. + * + * Examples: + * - A query to select rows that are expected to exist returns with `rows=[]`. + * - A query to insert a new record returns with `rowCount=0` indicating no new row was added. + * + * @export + * @class ApiExecuteSQLError + * @extends {ApiError} + */ +export class ApiExecuteSQLError extends ApiError { + constructor(message: string, errors?: (string | object)[]) { + super(ApiErrorType.EXECUTE_SQL, message, errors); + } +} + +export enum HTTPErrorType { + BAD_REQUEST = 'Bad Request', + UNAUTHORIZE = 'Unauthorized', + FORBIDDEN = 'Forbidden', + CONFLICT = 'Conflict', + INTERNAL_SERVER_ERROR = 'Internal Server Error' +} + +export class HTTPError extends Error { + status: number; + errors?: (string | object)[]; + + constructor(name: HTTPErrorType, status: number, message: string, errors?: (string | object)[], stack?: string) { + super(message); + + this.name = name; + this.status = status; + this.errors = errors || []; + + if (stack) { + this.stack = stack; + } + + if (!this.stack) { + Error.captureStackTrace(this); + } + } +} + +/** + * HTTP `400 Bad Request` error. + * + * @export + * @class HTTP400 + * @extends {HTTPError} + */ +export class HTTP400 extends HTTPError { + constructor(message: string, errors?: (string | object)[]) { + super(HTTPErrorType.BAD_REQUEST, 400, message, errors); + } +} + +/** + * HTTP `401 Unauthorized` error. + * + * @export + * @class HTTP401 + * @extends {HTTPError} + */ +export class HTTP401 extends HTTPError { + constructor(message: string, errors?: (string | object)[]) { + super(HTTPErrorType.UNAUTHORIZE, 401, message, errors); + } +} + +/** + * HTTP `403 Forbidden` error. + * + * @export + * @class HTTP403 + * @extends {HTTPError} + */ +export class HTTP403 extends HTTPError { + constructor(message: string, errors?: (string | object)[]) { + super(HTTPErrorType.FORBIDDEN, 403, message, errors); + } +} + +/** + * HTTP `409 Conflict` error. + * + * @export + * @class HTTP409 + * @extends {HTTPError} + */ +export class HTTP409 extends HTTPError { + constructor(message: string, errors?: (string | object)[]) { + super(HTTPErrorType.CONFLICT, 409, message, errors); + } +} + +/** + * HTTP `500 Internal Server Error` error. + * + * @export + * @class HTTP500 + * @extends {HTTPError} + */ +export class HTTP500 extends HTTPError { + constructor(message: string, errors?: (string | object)[]) { + super(HTTPErrorType.INTERNAL_SERVER_ERROR, 500, message, errors); + } +} + +/** + * Ensures that the incoming error is converted into an `HTTPError` if it is not one already. + * If `error` is a `HTTPError`, then change nothing and return it. + * If `error` is a `ApiError`, wrap it into an `HTTP500` error and return it. + * If `error` is a `DatabaseError`, wrap it into an `HTTP500` error and return it. + * If `error` is a `Error`, wrap it into an `HTTP500` error and return it. + * If `error` is none of the above, create a new generic `HTTP500` error and return it. + * + * @param {(HTTPError | ApiError | Error | any)} error + * @return {HTTPError} An instance of `HTTPError` + */ +export const ensureHTTPError = (error: HTTPError | ApiError | Error | any): HTTPError => { + if (error instanceof HTTPError) { + return error; + } + + if (error instanceof ApiError) { + return new HTTPError(HTTPErrorType.INTERNAL_SERVER_ERROR, 500, error.message, error.errors, error.stack); + } + + if (error instanceof DatabaseError) { + return new HTTPError( + HTTPErrorType.INTERNAL_SERVER_ERROR, + 500, + 'Unexpected Database Error', + [{ ...error, message: error.message }], + error.stack + ); + } + + if (error instanceof Error) { + return new HTTP500('Unexpected Error', [error.name, error.message]); + } + + return new HTTP500('Unexpected Error'); +}; diff --git a/api/src/json-schema/transformation-schema.test.ts b/api/src/json-schema/transformation-schema.test.ts index 23b1a75cad..e71d9a33c6 100644 --- a/api/src/json-schema/transformation-schema.test.ts +++ b/api/src/json-schema/transformation-schema.test.ts @@ -49,7 +49,8 @@ describe('example submission transformation schema', () => { { condition: { if: { - columns: ['Lone Cows'] + columns: ['Lone Cows'], + not: true } }, transformations: [ @@ -66,9 +67,12 @@ describe('example submission transformation schema', () => { eventDate: { columns: ['Date'] }, - verbatimCoordinates: { + verbatimCoordinatesUTM: { columns: ['UTM Zone', 'Easting', 'Northing'] }, + verbatimCoordinatesLatLong: { + columns: ['lat', 'long'] + }, occurrenceID: { columns: ['Waypoint'], unique: 'occ' @@ -109,9 +113,12 @@ describe('example submission transformation schema', () => { eventDate: { columns: ['Date'] }, - verbatimCoordinates: { + verbatimCoordinatesUTM: { columns: ['UTM Zone', 'Easting', 'Northing'] }, + verbatimCoordinatesLatLong: { + columns: ['lat', 'long'] + }, occurrenceID: { columns: ['Waypoint'], unique: 'occ' @@ -153,9 +160,12 @@ describe('example submission transformation schema', () => { eventDate: { columns: ['Date'] }, - verbatimCoordinates: { + verbatimCoordinatesUTM: { columns: ['UTM Zone', 'Easting', 'Northing'] }, + verbatimCoordinatesLatLong: { + columns: ['lat', 'long'] + }, occurrenceID: { columns: ['Waypoint'], unique: 'occ' @@ -199,40 +209,71 @@ describe('example submission transformation schema', () => { { fileName: 'event', columns: [ - { source: 'id', target: 'id' }, - { source: 'eventID', target: 'eventID' }, - { source: 'eventDate', target: 'eventDate' }, - { source: 'verbatimCoordinates', target: 'verbatimCoordinates' } + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['eventID'] }, target: 'eventID' }, + { source: { columns: ['eventDate'] }, target: 'eventDate' }, + { + source: { columns: ['verbatimCoordinatesUTM', 'verbatimCoordinatesLatLong'] }, + target: 'verbatimCoordinates' + } ] }, { fileName: 'occurrence', conditionalFields: ['individualCount'], columns: [ - { source: 'id', target: 'id' }, - { source: 'occurrenceID', target: 'occurrenceID' }, - { source: 'individualCount', target: 'individualCount' }, - { source: 'vernacularName', target: 'associatedTaxa' }, - { source: 'lifeStage', target: 'lifeStage' }, - { source: 'sex', target: 'sex' } + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['occurrenceID'] }, target: 'occurrenceID' }, + { source: { columns: ['individualCount'] }, target: 'individualCount' }, + { source: { columns: ['vernacularName'] }, target: 'associatedTaxa' }, + { source: { columns: ['lifeStage'] }, target: 'lifeStage' }, + { source: { columns: ['sex'] }, target: 'sex' }, + { source: { value: 'Approved' }, target: 'Status' } ] }, { fileName: 'taxon', columns: [ - { source: 'id', target: 'id' }, - { source: 'occurrenceID', target: 'occurrenceID' }, - { source: 'vernacularName', target: 'vernacularName' } + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['occurrenceID'] }, target: 'occurrenceID' }, + { source: { columns: ['vernacularName'] }, target: 'vernacularName' } ] }, { fileName: 'resourcerelationship', conditionalFields: ['resourceID'], columns: [ - { source: 'id', target: 'id' }, - { source: 'resourceID', target: 'resourceID' }, - { source: 'relatedResourceID', target: 'relatedResourceID' }, - { source: 'relationshipOfResource', target: 'relationshipOfResource' } + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['resourceID'] }, target: 'resourceID' }, + { source: { columns: ['relatedResourceID'] }, target: 'relatedResourceID' }, + { source: { columns: ['relationshipOfResource'] }, target: 'relationshipOfResource' } + ] + }, + { + fileName: 'measurementorfact', + columns: [ + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['eventID'] }, target: 'measurementID' }, + { source: { value: 'Habitat Description' }, target: 'measurementType' }, + { source: { columns: ['effort_habitat_description'] }, target: 'measurementValue' } + ] + }, + { + fileName: 'measurementorfact', + columns: [ + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['occurrenceID'] }, target: 'measurementID' }, + { source: { value: 'Stratum' }, target: 'measurementType' }, + { source: { columns: ['summary_stratum'] }, target: 'measurementValue' } + ] + }, + { + fileName: 'measurementorfact', + columns: [ + { source: { columns: ['id'] }, target: 'id' }, + { source: { columns: ['occurrenceID'] }, target: 'measurementID' }, + { source: { value: 'Activity' }, target: 'measurementType' }, + { source: { columns: ['observation_activity'] }, target: 'measurementValue' } ] } ] diff --git a/api/src/json-schema/transformation-schema.ts b/api/src/json-schema/transformation-schema.ts index f7b180757e..ec8ab8d67e 100644 --- a/api/src/json-schema/transformation-schema.ts +++ b/api/src/json-schema/transformation-schema.ts @@ -56,12 +56,16 @@ export const submissionTransformationSchema = { properties: { if: { type: 'object', + required: ['columns'], properties: { columns: { type: 'array', items: { type: 'string' } + }, + not: { + type: 'boolean' } }, additionalProperties: false @@ -112,7 +116,8 @@ export const submissionTransformationSchema = { } ] } - } + }, + additionalProperties: false } }, additionalProperties: false @@ -162,12 +167,43 @@ export const submissionTransformationSchema = { type: 'object', properties: { source: { - type: 'string' + oneOf: [ + { + type: 'object', + required: ['columns'], + properties: { + columns: { + type: 'array', + items: { + type: 'string' + } + }, + separator: { + type: 'string' + }, + unique: { + type: 'string' + } + }, + additionalProperties: false + }, + { + type: 'object', + required: ['value'], + properties: { + value: { + type: ['string', 'number'] + } + }, + additionalProperties: false + } + ] }, target: { type: 'string' } - } + }, + additionalProperties: false } }, conditionalFields: { diff --git a/api/src/models/gcnotify.ts b/api/src/models/gcnotify.ts new file mode 100644 index 0000000000..61e45de0e0 --- /dev/null +++ b/api/src/models/gcnotify.ts @@ -0,0 +1,16 @@ +export interface IgcNotifyPostReturn { + content: object; + id: string; + reference: string; + scheduled_for: string; + template: object; + uri: string; +} + +export interface IgcNotifyGenericMessage { + subject: string; + header: string; + body1: string; + body2: string; + footer: string; +} diff --git a/api/src/models/project-create.test.ts b/api/src/models/project-create.test.ts index 237eb5e707..1925ad1ece 100644 --- a/api/src/models/project-create.test.ts +++ b/api/src/models/project-create.test.ts @@ -1,16 +1,16 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { + PostCoordinatorData, + PostFundingData, + PostFundingSource, PostIUCNData, PostLocationData, PostObjectivesData, - PostCoordinatorData, PostPartnershipsData, PostPermitData, PostProjectData, - PostFundingData, - PostProjectObject, - PostFundingSource + PostProjectObject } from './project-create'; describe('PostProjectObject', () => { diff --git a/api/src/models/project-create.ts b/api/src/models/project-create.ts index 8ccb622262..2bf8bdc510 100644 --- a/api/src/models/project-create.ts +++ b/api/src/models/project-create.ts @@ -34,7 +34,7 @@ export class PostProjectObject { } /** - * Processes POST /project coordinator data + * Processes POST /project contact data * * @export * @class PostCoordinatorData diff --git a/api/src/models/project-survey-attachments.test.ts b/api/src/models/project-survey-attachments.test.ts index 5026b2090e..a6e7a2166c 100644 --- a/api/src/models/project-survey-attachments.test.ts +++ b/api/src/models/project-survey-attachments.test.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { GetAttachmentsData } from './project-survey-attachments'; +import { + GetAttachmentsData, + GetReportAttachmentAuthor, + GetReportAttachmentMetadata, + PostReportAttachmentMetadata, + PutReportAttachmentMetadata +} from './project-survey-attachments'; describe('GetAttachmentsData', () => { describe('No values provided', () => { @@ -47,3 +53,140 @@ describe('GetAttachmentsData', () => { }); }); }); + +describe('PostReportAttachmentsMetaData', () => { + describe('No values provided', () => { + let postReportAttachmentsData: PostReportAttachmentMetadata; + + before(() => { + postReportAttachmentsData = new PostReportAttachmentMetadata(null); + }); + + it('sets attachmentsData', function () { + expect(postReportAttachmentsData).to.eql({ title: null, year_published: 0, authors: [], description: null }); + }); + }); + + describe('All values provided', () => { + let postReportAttachmentsData: PostReportAttachmentMetadata; + + const input = { + title: 'Report 1', + year_published: 2000, + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'abstract of the report' + }; + + before(() => { + postReportAttachmentsData = new PostReportAttachmentMetadata(input); + }); + + it('sets the report metadata', function () { + expect(postReportAttachmentsData).to.eql({ + title: 'Report 1', + year_published: 2000, + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'abstract of the report' + }); + }); + }); +}); + +describe('PutReportAttachmentMetaData', () => { + describe('No values provided', () => { + it('sets attachmentsData', function () { + const putReportAttachmentData = new PutReportAttachmentMetadata(null); + + expect(putReportAttachmentData.title).to.equal(null); + expect(putReportAttachmentData.year_published).to.equal(0); + expect(putReportAttachmentData.authors).to.eql([]); + expect(putReportAttachmentData.description).to.equal(null); + expect(putReportAttachmentData.revision_count).to.equal(null); + }); + }); + + describe('All values provided', () => { + const input = { + title: 'Report 1', + year_published: 2000, + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'abstract of the report', + revision_count: 1 + }; + + it('sets the report metadata', function () { + const putReportAttachmentData = new PutReportAttachmentMetadata(input); + expect(putReportAttachmentData.title).to.equal(input.title); + expect(putReportAttachmentData.year_published).to.equal(input.year_published); + expect(putReportAttachmentData.authors).to.eql(input.authors); + expect(putReportAttachmentData.description).to.equal(input.description); + expect(putReportAttachmentData.revision_count).to.equal(input.revision_count); + }); + }); +}); + +describe('GetReportAttachmentMetaData', () => { + describe('No values provided', () => { + it('sets the report metadata', function () { + const getReportAttachmentData = new GetReportAttachmentMetadata(null); + + expect(getReportAttachmentData).to.eql({ + attachment_id: null, + title: null, + year_published: 0, + authors: [], + description: null, + last_modified: null, + revision_count: null + }); + }); + }); + + describe('All values provided', () => { + const input = { + attachment_id: 1, + title: 'My Report', + update_date: '2020-10-10', + description: 'abstract of the report', + year_published: 2020, + revision_count: 2, + authors: [{ first_name: 'John', last_name: 'Smith' }] + }; + + it('sets the report metadata', function () { + const getReportAttachmentData = new GetReportAttachmentMetadata(input); + + expect(getReportAttachmentData.title).to.equal(input.title); + expect(getReportAttachmentData.year_published).to.equal(input.year_published); + expect(getReportAttachmentData.description).to.equal(input.description); + expect(getReportAttachmentData.last_modified).to.equal(input.update_date); + expect(getReportAttachmentData.revision_count).to.equal(input.revision_count); + }); + }); +}); + +describe('GetReportAttachmentAuthor', () => { + describe('No values provided', () => { + it('sets the authors', function () { + const getReportAttachmentAuthor = new GetReportAttachmentAuthor(null); + expect(getReportAttachmentAuthor).to.eql({ + first_name: null, + last_name: null + }); + }); + }); + + describe('All values provided', () => { + const input = { + first_name: 'John', + last_name: 'Smith' + }; + + it('sets the report metadata', function () { + const getReportAttachmentAuthor = new GetReportAttachmentAuthor(input); + + expect(getReportAttachmentAuthor.first_name).to.equal(input.first_name); + expect(getReportAttachmentAuthor.last_name).to.equal(input.last_name); + }); + }); +}); diff --git a/api/src/models/project-survey-attachments.ts b/api/src/models/project-survey-attachments.ts index 8552a80dd8..2e24a73b45 100644 --- a/api/src/models/project-survey-attachments.ts +++ b/api/src/models/project-survey-attachments.ts @@ -21,7 +21,7 @@ export class GetAttachmentsData { id: item.id, fileName: item.file_name, fileType: item.file_type || 'Report', - lastModified: item.update_date || item.create_date, + lastModified: (item.update_date || item.create_date).toString(), size: item.file_size, securityToken: item.security_token }; @@ -29,3 +29,70 @@ export class GetAttachmentsData { []; } } + +export interface IReportAttachmentAuthor { + first_name: string; + last_name: string; +} + +export class PostReportAttachmentMetadata { + title: string; + year_published: number; + authors: IReportAttachmentAuthor[]; + description: string; + + constructor(obj?: any) { + this.title = (obj && obj?.title) || null; + this.year_published = Number((obj && obj?.year_published) || null); + this.authors = (obj?.authors?.length && obj.authors) || []; + this.description = (obj && obj?.description) || null; + } +} + +export class PutReportAttachmentMetadata extends PostReportAttachmentMetadata { + revision_count: number; + + constructor(obj?: any) { + super(obj); + + this.revision_count = (obj && obj?.revision_count) || null; + } +} + +export class GetReportAttachmentMetadata { + attachment_id: number; + title: string; + last_modified: string; + description: string; + year_published: number; + revision_count: number; + authors: IReportAttachmentAuthor[]; + + constructor(metaObj?: any, authorObj?: any) { + this.attachment_id = (metaObj && metaObj?.attachment_id) || null; + this.title = (metaObj && metaObj?.title) || null; + this.last_modified = (metaObj && metaObj?.update_date.toString()) || null; + this.description = (metaObj && metaObj?.description) || null; + this.year_published = Number((metaObj && metaObj?.year_published) || null); + this.revision_count = (metaObj && metaObj?.revision_count) || null; + this.authors = + (authorObj && + authorObj?.map((author: any) => { + return { + first_name: author?.first_name, + last_name: author?.last_name + }; + })) || + []; + } +} + +export class GetReportAttachmentAuthor { + first_name: string; + last_name: string; + + constructor(authorObj?: any) { + this.first_name = (authorObj && authorObj?.first_name) || null; + this.last_name = (authorObj && authorObj?.last_name) || null; + } +} diff --git a/api/src/models/project-update.test.ts b/api/src/models/project-update.test.ts index 7525259c3c..85293395a0 100644 --- a/api/src/models/project-update.test.ts +++ b/api/src/models/project-update.test.ts @@ -1,494 +1,41 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { - GetCoordinatorData, - GetPartnershipsData, - GetObjectivesData, PutCoordinatorData, - PutPartnershipsData, - PutObjectivesData, - GetLocationData, - GetProjectData, - PutProjectData, + PutFundingSource, PutIUCNData, - GetIUCNClassificationData, PutLocationData, - PutFundingSource, - GetPermitData + PutObjectivesData, + PutPartnershipsData, + PutProjectData } from './project-update'; -describe('PutLocationData', () => { - describe('No values provided', () => { - let data: PutLocationData; - - before(() => { - data = new PutLocationData(null); - }); - - it('sets location_description', () => { - expect(data.location_description).to.equal(null); - }); - - it('sets geometry', () => { - expect(data.geometry).to.eql([]); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.eql(null); - }); - }); - - describe('All values provided', () => { - let data: PutLocationData; - - const obj = { - location_description: 'location', - geometry: [ - { - type: 'Polygon', - coordinates: [ - [ - [-128, 55], - [-128, 55.5], - [-128, 56], - [-126, 58], - [-128, 55] - ] - ], - properties: { - name: 'Biohub Islands' - } - } - ], - revision_count: 1 - }; - - before(() => { - data = new PutLocationData(obj); - }); - - it('sets location_description', () => { - expect(data.location_description).to.equal(obj.location_description); - }); - - it('sets geometry', () => { - expect(data.geometry).to.eql(obj.geometry); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.eql(obj.revision_count); - }); - }); -}); - -describe('GetIUCNClassificationData', () => { - describe('No values provided', () => { - let data: GetIUCNClassificationData; - - before(() => { - data = new GetIUCNClassificationData((null as unknown) as any[]); - }); - - it('sets classification details', () => { - expect(data.classificationDetails).to.eql([]); - }); - }); - - describe('All values provided', () => { - const obj = [ - { - classification: 1, - subclassification1: 2, - subclassification2: 2 - } - ]; - - let data: GetIUCNClassificationData; - - before(() => { - data = new GetIUCNClassificationData(obj); - }); - - it('sets classification details', () => { - expect(data.classificationDetails).to.eql([ - { - classification: 1, - subClassification1: 2, - subClassification2: 2 - } - ]); - }); - }); -}); - -describe('PutIUCNData', () => { - describe('No values provided', () => { - let data: PutIUCNData; - - before(() => { - data = new PutIUCNData(null); - }); - - it('sets classification details', () => { - expect(data.classificationDetails).to.eql([]); - }); - }); - - describe('All values provided', () => { - const obj = { - classificationDetails: [ - { - classification: 1, - subClassification1: 2, - subClassification2: 2 - } - ] - }; - - let data: PutIUCNData; - - before(() => { - data = new PutIUCNData(obj); - }); - - it('sets classification details', () => { - expect(data.classificationDetails).to.eql(obj.classificationDetails); - }); - }); -}); - -describe('PutPartnershipsData', () => { - describe('No values provided', () => { - let data: PutPartnershipsData; - - before(() => { - data = new PutPartnershipsData(null); - }); - - it('sets indigenous_partnerships', () => { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', () => { - expect(data.stakeholder_partnerships).to.eql([]); - }); - }); - - describe('all values provided', () => { - const obj = { - indigenous_partnerships: [1, 2], - stakeholder_partnerships: ['partner 3', 'partner 4'] - }; - - let data: PutPartnershipsData; - - before(() => { - data = new PutPartnershipsData(obj); - }); - - it('sets indigenous_partnerships', () => { - expect(data.indigenous_partnerships).to.eql(obj.indigenous_partnerships); - }); - - it('sets stakeholder_partnerships', () => { - expect(data.stakeholder_partnerships).to.eql(obj.stakeholder_partnerships); - }); - }); -}); - -describe('GetPartnershipsData', () => { - describe('No values provided', () => { - let data: GetPartnershipsData; - - before(() => { - data = new GetPartnershipsData((null as unknown) as any[], (null as unknown) as any[]); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); - }); - }); - - describe('Empty arrays as values provided', () => { - let data: GetPartnershipsData; - - before(() => { - data = new GetPartnershipsData([], []); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); - }); - }); - - describe('indigenous_partnerships values provided', () => { - let data: GetPartnershipsData; - - const indigenous_partnerships = [{ id: 1 }, { id: 2 }]; - const stakeholder_partnerships: string[] = []; - - before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([1, 2]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); - }); - }); - - describe('stakeholder_partnerships values provided', () => { - let data: GetPartnershipsData; - - const indigenous_partnerships: string[] = []; - const stakeholder_partnerships = [{ name: 'partner 1' }, { name: 'partner 2' }]; - - before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql(['partner 1', 'partner 2']); - }); - }); - - describe('All values provided', () => { - let data: GetPartnershipsData; - - const indigenous_partnerships = [{ id: 1 }, { id: 2 }]; - const stakeholder_partnerships = [{ name: 'partner 3' }, { name: 'partner 4' }]; - - before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([1, 2]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql(['partner 3', 'partner 4']); - }); - }); -}); - -describe('GetCoordinatorData', () => { - describe('No values provided', () => { - let data: GetCoordinatorData; - - before(() => { - data = new GetCoordinatorData(null); - }); - - it('sets first_name', () => { - expect(data.first_name).to.equal(null); - }); - - it('sets last_name', () => { - expect(data.last_name).to.equal(null); - }); - - it('sets email_address', () => { - expect(data.email_address).to.equal(null); - }); - - it('sets coordinator_agency', () => { - expect(data.coordinator_agency).to.equal(null); - }); - - it('sets share_contact_details', () => { - expect(data.share_contact_details).to.equal('false'); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(null); - }); - }); - - describe('all values provided', () => { - const obj = { - coordinator_first_name: 'coordinator_first_name', - coordinator_last_name: 'coordinator_last_name', - coordinator_email_address: 'coordinator_email_address', - coordinator_agency_name: 'coordinator_agency_name', - coordinator_public: true, - revision_count: 1 - }; - - let data: GetCoordinatorData; - - before(() => { - data = new GetCoordinatorData(obj); - }); - - it('sets first_name', () => { - expect(data.first_name).to.equal('coordinator_first_name'); - }); - - it('sets last_name', () => { - expect(data.last_name).to.equal('coordinator_last_name'); - }); - - it('sets email_address', () => { - expect(data.email_address).to.equal('coordinator_email_address'); - }); - - it('sets coordinator_agency', () => { - expect(data.coordinator_agency).to.equal('coordinator_agency_name'); - }); - - it('sets share_contact_details', () => { - expect(data.share_contact_details).to.equal('true'); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(1); - }); - }); -}); - -describe('PutCoordinatorData', () => { - describe('No values provided', () => { - let data: PutCoordinatorData; - - before(() => { - data = new PutCoordinatorData(null); - }); - - it('sets first_name', () => { - expect(data.first_name).to.equal(null); - }); - - it('sets last_name', () => { - expect(data.last_name).to.equal(null); - }); - - it('sets email_address', () => { - expect(data.email_address).to.equal(null); - }); - - it('sets coordinator_agency', () => { - expect(data.coordinator_agency).to.equal(null); - }); - - it('sets share_contact_details', () => { - expect(data.share_contact_details).to.equal(false); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(null); - }); - }); - - describe('all values provided', () => { - const obj = { - first_name: 'coordinator_first_name', - last_name: 'coordinator_last_name', - email_address: 'coordinator_email_address', - coordinator_agency: 'coordinator_agency_name', - share_contact_details: 'true', - revision_count: 1 - }; - - let data: PutCoordinatorData; - - before(() => { - data = new PutCoordinatorData(obj); - }); - - it('sets first_name', () => { - expect(data.first_name).to.equal('coordinator_first_name'); - }); - - it('sets last_name', () => { - expect(data.last_name).to.equal('coordinator_last_name'); - }); - - it('sets email_address', () => { - expect(data.email_address).to.equal('coordinator_email_address'); - }); - - it('sets coordinator_agency', () => { - expect(data.coordinator_agency).to.equal('coordinator_agency_name'); - }); - - it('sets share_contact_details', () => { - expect(data.share_contact_details).to.equal(true); - }); - - it('sets revision_count', () => { - expect(data.revision_count).to.equal(1); - }); - }); -}); - -describe('GetPermitData', () => { +describe('PutProjectData', () => { describe('No values provided', () => { - let projectPermitData: GetPermitData; + let data: PutProjectData; before(() => { - projectPermitData = new GetPermitData((null as unknown) as any[]); - }); - - it('sets permits', function () { - expect(projectPermitData.permits).to.eql([]); + data = new PutProjectData(); }); - }); - describe('All values provided', () => { - let projectPermitData: GetPermitData; - - const permits = [ - { - number: '1', - type: 'permit type' - } - ]; - - before(() => { - projectPermitData = new GetPermitData(permits); + it('sets name', () => { + expect(data.name).to.equal(null); }); - it('sets permits', function () { - expect(projectPermitData.permits).to.eql([ - { - permit_number: '1', - permit_type: 'permit type' - } - ]); + it('sets type', () => { + expect(data.type).to.equal(null); }); - }); -}); -describe('GetObjectivesData', () => { - describe('No values provided', () => { - let data: GetObjectivesData; - - before(() => { - data = new GetObjectivesData(null); + it('sets project_activities', () => { + expect(data.project_activities).to.eql([]); }); - it('sets objectives', () => { - expect(data.objectives).to.equal(''); + it('sets start_date', () => { + expect(data.start_date).to.equal(null); }); - - it('sets caveats', () => { - expect(data.caveats).to.equal(''); + + it('sets end_date', () => { + expect(data.end_date).to.equal(null); }); it('sets revision_count', () => { @@ -498,27 +45,42 @@ describe('GetObjectivesData', () => { describe('all values provided', () => { const obj = { - objectives: 'objectives', - caveats: 'caveats', + project_name: 'project name', + project_type: 4, + project_activities: [1, 2], + start_date: '2020-04-20T07:00:00.000Z', + end_date: '2020-05-20T07:00:00.000Z', revision_count: 1 }; - let data: GetObjectivesData; + let data: PutProjectData; before(() => { - data = new GetObjectivesData(obj); + data = new PutProjectData(obj); }); - it('sets objectives', () => { - expect(data.objectives).to.equal(obj.objectives); + it('sets name', () => { + expect(data.name).to.equal('project name'); }); - it('sets caveats', () => { - expect(data.caveats).to.equal(obj.caveats); + it('sets type', () => { + expect(data.type).to.equal(4); + }); + + it('sets project_activities', () => { + expect(data.project_activities).to.eql([1, 2]); + }); + + it('sets start_date', () => { + expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); + }); + + it('sets end_date', () => { + expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); }); it('sets revision_count', () => { - expect(data.revision_count).to.equal(obj.revision_count); + expect(data.revision_count).to.equal(1); }); }); }); @@ -571,102 +133,32 @@ describe('PutObjectivesData', () => { }); }); -describe('GetLocationData', () => { - describe('No values provided', () => { - let locationData: GetLocationData; - - before(() => { - locationData = new GetLocationData(null); - }); - - it('sets location_description', function () { - expect(locationData.location_description).to.equal(''); - }); - - it('sets the geometry', function () { - expect(locationData.geometry).to.eql([]); - }); - - it('sets revision_count', () => { - expect(locationData.revision_count).to.equal(null); - }); - }); - - describe('All values provided', () => { - let locationData: GetLocationData; - - const location_description = 'location description'; - const geometry = [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [125.6, 10.1] - }, - properties: { - name: 'Dinagat Islands' - } - } - ]; - const revision_count = 1; - - const locationDataObj = [ - { - location_description, - geometry, - revision_count - }, - { - location_description, - geometry, - revision_count - } - ]; - - before(() => { - locationData = new GetLocationData(locationDataObj); - }); - - it('sets location_description', function () { - expect(locationData.location_description).to.equal(location_description); - }); - - it('sets the geometry', function () { - expect(locationData.geometry).to.eql(geometry); - }); - - it('sets revision_count', () => { - expect(locationData.revision_count).to.equal(revision_count); - }); - }); -}); - -describe('GetProjectData', () => { +describe('PutCoordinatorData', () => { describe('No values provided', () => { - let data: GetProjectData; + let data: PutCoordinatorData; before(() => { - data = new GetProjectData(); + data = new PutCoordinatorData(null); }); - it('sets name', () => { - expect(data.project_name).to.equal(''); + it('sets first_name', () => { + expect(data.first_name).to.equal(null); }); - it('sets type', () => { - expect(data.project_type).to.equal(''); + it('sets last_name', () => { + expect(data.last_name).to.equal(null); }); - it('sets project_activities', () => { - expect(data.project_activities).to.eql([]); + it('sets email_address', () => { + expect(data.email_address).to.equal(null); }); - it('sets start_date', () => { - expect(data.start_date).to.equal(''); + it('sets coordinator_agency', () => { + expect(data.coordinator_agency).to.equal(null); }); - it('sets end_date', () => { - expect(data.end_date).to.equal(''); + it('sets share_contact_details', () => { + expect(data.share_contact_details).to.equal(false); }); it('sets revision_count', () => { @@ -675,40 +167,39 @@ describe('GetProjectData', () => { }); describe('all values provided', () => { - const projectData = { - name: 'project name', - pt_id: 4, - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', + const obj = { + first_name: 'coordinator_first_name', + last_name: 'coordinator_last_name', + email_address: 'coordinator_email_address', + coordinator_agency: 'coordinator_agency_name', + share_contact_details: 'true', revision_count: 1 }; - const activityData = [{ activity_id: 1 }, { activity_id: 2 }]; - - let data: GetProjectData; + let data: PutCoordinatorData; before(() => { - data = new GetProjectData(projectData, activityData); + data = new PutCoordinatorData(obj); }); - it('sets name', () => { - expect(data.project_name).to.equal('project name'); + it('sets first_name', () => { + expect(data.first_name).to.equal('coordinator_first_name'); }); - it('sets type', () => { - expect(data.project_type).to.equal(4); + it('sets last_name', () => { + expect(data.last_name).to.equal('coordinator_last_name'); }); - it('sets project_activities', () => { - expect(data.project_activities).to.eql([1, 2]); + it('sets email_address', () => { + expect(data.email_address).to.equal('coordinator_email_address'); }); - it('sets start_date', () => { - expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); + it('sets coordinator_agency', () => { + expect(data.coordinator_agency).to.equal('coordinator_agency_name'); }); - it('sets end_date', () => { - expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); + it('sets share_contact_details', () => { + expect(data.share_contact_details).to.equal(true); }); it('sets revision_count', () => { @@ -717,77 +208,102 @@ describe('GetProjectData', () => { }); }); -describe('PutProjectData', () => { +describe('PutLocationData', () => { describe('No values provided', () => { - let data: PutProjectData; + let data: PutLocationData; before(() => { - data = new PutProjectData(); - }); - - it('sets name', () => { - expect(data.name).to.equal(null); - }); - - it('sets type', () => { - expect(data.type).to.equal(null); - }); - - it('sets project_activities', () => { - expect(data.project_activities).to.eql([]); + data = new PutLocationData(null); }); - it('sets start_date', () => { - expect(data.start_date).to.equal(null); + it('sets location_description', () => { + expect(data.location_description).to.equal(null); }); - it('sets end_date', () => { - expect(data.end_date).to.equal(null); + it('sets geometry', () => { + expect(data.geometry).to.eql([]); }); it('sets revision_count', () => { - expect(data.revision_count).to.equal(null); + expect(data.revision_count).to.eql(null); }); }); - describe('all values provided', () => { + describe('All values provided', () => { + let data: PutLocationData; + const obj = { - project_name: 'project name', - project_type: 4, - project_activities: [1, 2], - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', + location_description: 'location', + geometry: [ + { + type: 'Polygon', + coordinates: [ + [ + [-128, 55], + [-128, 55.5], + [-128, 56], + [-126, 58], + [-128, 55] + ] + ], + properties: { + name: 'Biohub Islands' + } + } + ], revision_count: 1 }; - let data: PutProjectData; - before(() => { - data = new PutProjectData(obj); + data = new PutLocationData(obj); }); - it('sets name', () => { - expect(data.name).to.equal('project name'); + it('sets location_description', () => { + expect(data.location_description).to.equal(obj.location_description); }); - it('sets type', () => { - expect(data.type).to.equal(4); + it('sets geometry', () => { + expect(data.geometry).to.eql(obj.geometry); }); - it('sets project_activities', () => { - expect(data.project_activities).to.eql([1, 2]); + it('sets revision_count', () => { + expect(data.revision_count).to.eql(obj.revision_count); }); + }); +}); - it('sets start_date', () => { - expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); +describe('PutIUCNData', () => { + describe('No values provided', () => { + let data: PutIUCNData; + + before(() => { + data = new PutIUCNData(null); }); - it('sets end_date', () => { - expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); + it('sets classification details', () => { + expect(data.classificationDetails).to.eql([]); }); + }); - it('sets revision_count', () => { - expect(data.revision_count).to.equal(1); + describe('All values provided', () => { + const obj = { + classificationDetails: [ + { + classification: 1, + subClassification1: 2, + subClassification2: 2 + } + ] + }; + + let data: PutIUCNData; + + before(() => { + data = new PutIUCNData(obj); + }); + + it('sets classification details', () => { + expect(data.classificationDetails).to.eql(obj.classificationDetails); }); }); }); @@ -877,3 +393,42 @@ describe('PutFundingSource', () => { }); }); }); + +describe('PutPartnershipsData', () => { + describe('No values provided', () => { + let data: PutPartnershipsData; + + before(() => { + data = new PutPartnershipsData(null); + }); + + it('sets indigenous_partnerships', () => { + expect(data.indigenous_partnerships).to.eql([]); + }); + + it('sets stakeholder_partnerships', () => { + expect(data.stakeholder_partnerships).to.eql([]); + }); + }); + + describe('all values provided', () => { + const obj = { + indigenous_partnerships: [1, 2], + stakeholder_partnerships: ['partner 3', 'partner 4'] + }; + + let data: PutPartnershipsData; + + before(() => { + data = new PutPartnershipsData(obj); + }); + + it('sets indigenous_partnerships', () => { + expect(data.indigenous_partnerships).to.eql(obj.indigenous_partnerships); + }); + + it('sets stakeholder_partnerships', () => { + expect(data.stakeholder_partnerships).to.eql(obj.stakeholder_partnerships); + }); + }); +}); diff --git a/api/src/models/project-update.ts b/api/src/models/project-update.ts index 9a9442e5f1..0130eddd31 100644 --- a/api/src/models/project-update.ts +++ b/api/src/models/project-update.ts @@ -3,25 +3,6 @@ import { getLogger } from '../utils/logger'; const defaultLog = getLogger('models/project-update'); -export class PutIUCNData { - classificationDetails: IGetPutIUCN[]; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PutIUCNData', message: 'params', obj }); - - this.classificationDetails = - (obj?.classificationDetails?.length && - obj.classificationDetails.map((item: any) => { - return { - classification: item.classification, - subClassification1: item.subClassification1, - subClassification2: item.subClassification2 - }; - })) || - []; - } -} - export class PutProjectData { name: string; type: number; @@ -42,29 +23,6 @@ export class PutProjectData { } } -export class PutLocationData { - location_description: string; - geometry: Feature[]; - revision_count: number; - - constructor(obj?: any) { - defaultLog.debug({ - label: 'PutLocationData', - message: 'params', - obj: { - ...obj, - geometry: obj?.geometry?.map((item: any) => { - return { ...item, geometry: 'Too big to print' }; - }) - } - }); - - this.location_description = (obj && obj.location_description) || null; - this.geometry = (obj?.geometry?.length && obj.geometry) || []; - this.revision_count = obj?.revision_count ?? null; - } -} - export class PutObjectivesData { objectives: string; caveats: string; @@ -99,168 +57,54 @@ export class PutCoordinatorData { } } -export class PutPartnershipsData { - indigenous_partnerships: number[]; - stakeholder_partnerships: string[]; - - constructor(obj?: any) { - defaultLog.debug({ label: 'PutPartnershipsData', message: 'params', obj }); - - this.indigenous_partnerships = (obj?.indigenous_partnerships?.length && obj.indigenous_partnerships) || []; - this.stakeholder_partnerships = (obj?.stakeholder_partnerships?.length && obj.stakeholder_partnerships) || []; - } -} - -export class GetCoordinatorData { - first_name: string; - last_name: string; - email_address: string; - coordinator_agency: string; - share_contact_details: string; +export class PutLocationData { + location_description: string; + geometry: Feature[]; revision_count: number; constructor(obj?: any) { - defaultLog.debug({ label: 'GetCoordinatorData', message: 'params', obj }); - - this.first_name = obj?.coordinator_first_name || null; - this.last_name = obj?.coordinator_last_name || null; - this.email_address = obj?.coordinator_email_address || null; - this.coordinator_agency = obj?.coordinator_agency_name || null; - this.share_contact_details = (obj?.coordinator_public && 'true') || 'false'; - this.revision_count = obj?.revision_count ?? null; - } -} - -/** - * Pre-processes GET /projects/{id} partnerships data for editing purposes - * - * @export - * @class GetPartnershipsData - */ -export class GetPartnershipsData { - indigenous_partnerships: number[]; - stakeholder_partnerships: string[]; - - constructor(indigenous_partnerships?: any[], stakeholder_partnerships?: any[]) { defaultLog.debug({ - label: 'GetPartnershipsData', + label: 'PutLocationData', message: 'params', - indigenous_partnerships, - stakeholder_partnerships + obj: { + ...obj, + geometry: obj?.geometry?.map((item: any) => { + return { ...item, geometry: 'Too big to print' }; + }) + } }); - this.indigenous_partnerships = - (indigenous_partnerships?.length && indigenous_partnerships.map((item: any) => item.id)) || []; - this.stakeholder_partnerships = - (stakeholder_partnerships?.length && stakeholder_partnerships.map((item: any) => item.name)) || []; + this.location_description = (obj && obj.location_description) || null; + this.geometry = (obj?.geometry?.length && obj.geometry) || []; + this.revision_count = obj?.revision_count ?? null; } } -export interface IGetPutIUCN { +export interface IPutIUCN { classification: number; subClassification1: number; subClassification2: number; } -/** - * Pre-processes GET /projects/{id} IUCN classification data for editing purposes - * - * @export - * @class GetIUCNClassificationData - */ -export class GetIUCNClassificationData { - classificationDetails: IGetPutIUCN[]; +export class PutIUCNData { + classificationDetails: IPutIUCN[]; - constructor(iucnClassificationData?: any[]) { - defaultLog.debug({ - label: 'GetIUCNClassificationData', - message: 'params', - iucnClassificationData: iucnClassificationData - }); + constructor(obj?: any) { + defaultLog.debug({ label: 'PutIUCNData', message: 'params', obj }); this.classificationDetails = - (iucnClassificationData && - iucnClassificationData.map((item: any) => { + (obj?.classificationDetails?.length && + obj.classificationDetails.map((item: any) => { return { classification: item.classification, - subClassification1: item.subclassification1, - subClassification2: item.subclassification2 + subClassification1: item.subClassification1, + subClassification2: item.subClassification2 }; })) || []; } } -export class GetObjectivesData { - objectives: string; - caveats: string; - revision_count: number; - - constructor(obj?: any) { - defaultLog.debug({ label: 'GetObjectivesData', message: 'params', obj }); - - this.objectives = obj?.objectives || ''; - this.caveats = obj?.caveats || ''; - this.revision_count = obj?.revision_count ?? null; - } -} - -/** - * Pre-processes GET /projects/{id} location data - * - * @export - * @class GetLocationData - */ -export class GetLocationData { - location_description: string; - geometry?: Feature[]; - revision_count: number; - - constructor(locationData?: any) { - defaultLog.debug({ - label: 'GetLocationData', - message: 'params', - locationData: locationData?.map((item: any) => { - return { ...item, geometry: 'Too big to print' }; - }) - }); - - const locationDataItem = locationData && locationData.length && locationData[0]; - - this.location_description = locationDataItem?.location_description || ''; - this.geometry = (locationDataItem?.geometry?.length && locationDataItem.geometry) || []; - this.revision_count = locationDataItem?.revision_count ?? null; - } -} - -/** - * Pre-processes GET /projects/{projectId}/update project data - * - * @export - * @class GetProjectData - */ -export class GetProjectData { - project_name: string; - project_type: number; - project_activities: number[]; - start_date: string; - end_date: string; - revision_count: number; - publish_date: string; - - constructor(projectData?: any, activityData?: any[]) { - defaultLog.debug({ label: 'GetProjectData', message: 'params', projectData, activityData }); - - this.project_name = projectData?.name || ''; - this.project_type = projectData?.pt_id || ''; - this.project_activities = (activityData?.length && activityData.map((item) => item.activity_id)) || []; - this.start_date = projectData?.start_date || ''; - this.end_date = projectData?.end_date || ''; - this.revision_count = projectData?.revision_count ?? null; - this.publish_date = projectData?.publish_date || ''; - } -} - export class PutFundingSource { id: number; investment_action_category: number; @@ -285,35 +129,14 @@ export class PutFundingSource { } } -interface IGetPermit { - permit_number: string; - permit_type: string; -} - -/** - * Pre-processes GET /projects/{projectId}/update permit data - * - * @export - * @class GetPermitData - */ -export class GetPermitData { - permits: IGetPermit[]; +export class PutPartnershipsData { + indigenous_partnerships: number[]; + stakeholder_partnerships: string[]; - constructor(permitData?: any[]) { - defaultLog.debug({ - label: 'GetPermitData', - message: 'params', - permitData: permitData - }); + constructor(obj?: any) { + defaultLog.debug({ label: 'PutPartnershipsData', message: 'params', obj }); - this.permits = - (permitData?.length && - permitData.map((item: any) => { - return { - permit_number: item.number, - permit_type: item.type - }; - })) || - []; + this.indigenous_partnerships = (obj?.indigenous_partnerships?.length && obj.indigenous_partnerships) || []; + this.stakeholder_partnerships = (obj?.stakeholder_partnerships?.length && obj.stakeholder_partnerships) || []; } } diff --git a/api/src/models/project-view-update.test.ts b/api/src/models/project-view-update.test.ts deleted file mode 100644 index 5de990969e..0000000000 --- a/api/src/models/project-view-update.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { GetFundingData } from './project-view-update'; - -describe('GetFundingData', () => { - describe('No values provided', () => { - let fundingData: GetFundingData; - - before(() => { - fundingData = new GetFundingData((null as unknown) as any[]); - }); - - it('sets project funding sources', function () { - expect(fundingData.fundingSources).to.eql([]); - }); - }); - - describe('No length for funding data provided', () => { - let fundingData: GetFundingData; - - before(() => { - fundingData = new GetFundingData([]); - }); - - it('sets project funding sources', function () { - expect(fundingData.fundingSources).to.eql([]); - }); - }); - - describe('All values provided', () => { - let fundingData: GetFundingData; - - const fundingDataObj = [ - { - id: 1, - agency_id: '1', - agency_name: 'Agency name', - agency_project_id: 'Agency123', - investment_action_category: 'Investment', - investment_action_category_name: 'Investment name', - start_date: '01/01/2020', - end_date: '01/01/2021', - funding_amount: 123, - revision_count: 0 - } - ]; - - before(() => { - fundingData = new GetFundingData(fundingDataObj); - }); - - it('sets project funding sources', function () { - expect(fundingData.fundingSources).to.eql(fundingDataObj); - }); - }); -}); diff --git a/api/src/models/project-view-update.ts b/api/src/models/project-view-update.ts deleted file mode 100644 index 8581303593..0000000000 --- a/api/src/models/project-view-update.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { getLogger } from '../utils/logger'; - -const defaultLog = getLogger('models/project-view-update'); - -interface IGetFundingSource { - id: number; - agency_id: number; - investment_action_category: number; - investment_action_category_name: string; - agency_name: string; - funding_amount: number; - start_date: string; - end_date: string; - agency_project_id: string; - revision_count: number; -} - -export class GetFundingData { - fundingSources: IGetFundingSource[]; - - constructor(fundingData?: any[]) { - defaultLog.debug({ - label: 'GetFundingData', - message: 'params', - fundingData: fundingData - }); - - this.fundingSources = - (fundingData && - fundingData.map((item: any) => { - return { - id: item.id, - agency_id: item.agency_id, - investment_action_category: item.investment_action_category, - investment_action_category_name: item.investment_action_category_name, - agency_name: item.agency_name, - funding_amount: item.funding_amount, - start_date: item.start_date, - end_date: item.end_date, - agency_project_id: item.agency_project_id, - revision_count: item.revision_count - }; - })) || - []; - } -} diff --git a/api/src/models/project-view.test.ts b/api/src/models/project-view.test.ts index f29c74cfc8..bf68d2862d 100644 --- a/api/src/models/project-view.test.ts +++ b/api/src/models/project-view.test.ts @@ -1,8 +1,9 @@ import { expect } from 'chai'; -import { COMPLETION_STATUS } from '../constants/status'; import { describe } from 'mocha'; +import { COMPLETION_STATUS } from '../constants/status'; import { GetCoordinatorData, + GetFundingData, GetIUCNClassificationData, GetLocationData, GetObjectivesData, @@ -11,145 +12,78 @@ import { GetProjectData } from './project-view'; -describe('GetPartnershipsData', () => { +describe('GetProjectData', () => { describe('No values provided', () => { - let data: GetPartnershipsData; + let data: GetProjectData; before(() => { - data = new GetPartnershipsData((null as unknown) as any[], (null as unknown) as any[]); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); + data = new GetProjectData(); }); - }); - describe('Empty arrays as values provided', () => { - let data: GetPartnershipsData; - - before(() => { - data = new GetPartnershipsData([], []); + it('sets name', () => { + expect(data.project_name).to.equal(''); }); - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); + it('sets type', () => { + expect(data.project_type).to.equal(-1); }); - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); + it('sets project_activities', () => { + expect(data.project_activities).to.eql([]); }); - }); - describe('indigenous_partnerships values provided', () => { - let data: GetPartnershipsData; - - const indigenous_partnerships = [{ fn_name: 'partner 1' }, { fn_name: 'partner 2' }]; - const stakeholder_partnerships: string[] = []; - - before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); + it('sets start_date', () => { + expect(data.start_date).to.equal(''); }); - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql(['partner 1', 'partner 2']); + it('sets end_date', () => { + expect(data.end_date).to.equal(''); }); - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql([]); + it('sets completion_status', () => { + expect(data.completion_status).to.equal(COMPLETION_STATUS.ACTIVE); }); }); - describe('stakeholder_partnerships values provided', () => { - let data: GetPartnershipsData; - - const indigenous_partnerships: string[] = []; - const stakeholder_partnerships = [{ sp_name: 'partner 1' }, { sp_name: 'partner 2' }]; - - before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql([]); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql(['partner 1', 'partner 2']); - }); - }); + describe('all values provided', () => { + const projectData = { + name: 'project name', + pt_id: 4, + start_date: '2020-04-20T07:00:00.000Z', + end_date: '2020-05-20T07:00:00.000Z', + revision_count: 1 + }; - describe('All values provided', () => { - let data: GetPartnershipsData; + const activityData = [{ activity_id: 1 }, { activity_id: 2 }]; - const indigenous_partnerships = [{ fn_name: 'partner 1' }, { fn_name: 'partner 2' }]; - const stakeholder_partnerships = [{ sp_name: 'partner 3' }, { sp_name: 'partner 4' }]; + let data: GetProjectData; before(() => { - data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); - }); - - it('sets indigenous_partnerships', function () { - expect(data.indigenous_partnerships).to.eql(['partner 1', 'partner 2']); - }); - - it('sets stakeholder_partnerships', function () { - expect(data.stakeholder_partnerships).to.eql(['partner 3', 'partner 4']); + data = new GetProjectData(projectData, activityData); }); - }); -}); -describe('GetIUCNClassificationData', () => { - describe('No values provided', () => { - let iucnClassificationData: GetIUCNClassificationData; - - before(() => { - iucnClassificationData = new GetIUCNClassificationData((null as unknown) as any[]); + it('sets name', () => { + expect(data.project_name).to.equal(projectData.name); }); - it('sets classification details', function () { - expect(iucnClassificationData.classificationDetails).to.eql([]); + it('sets type', () => { + expect(data.project_type).to.equal(projectData.pt_id); }); - }); - - describe('Empty array as values provided', () => { - let iucnClassificationData: GetIUCNClassificationData; - before(() => { - iucnClassificationData = new GetIUCNClassificationData([]); + it('sets project_activities', () => { + expect(data.project_activities).to.eql([1, 2]); }); - it('sets classification details', function () { - expect(iucnClassificationData.classificationDetails).to.eql([]); + it('sets start_date', () => { + expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); }); - }); - - describe('All values provided', () => { - let iucnClassificationData: GetIUCNClassificationData; - const iucnClassificationDataObj = [ - { - classification: 'class', - subclassification1: 'subclass1', - subclassification2: 'subclass2' - } - ]; - - before(() => { - iucnClassificationData = new GetIUCNClassificationData(iucnClassificationDataObj); + it('sets end_date', () => { + expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); }); - it('sets classification details', function () { - expect(iucnClassificationData.classificationDetails).to.eql([ - { - classification: 'class', - subClassification1: 'subclass1', - subClassification2: 'subclass2' - } - ]); + it('sets completion_status', () => { + expect(data.completion_status).to.equal(COMPLETION_STATUS.COMPLETED); }); }); }); @@ -259,6 +193,44 @@ describe('GetCoordinatorData', () => { }); }); +describe('GetPermitData', () => { + describe('No values provided', () => { + let projectPermitData: GetPermitData; + + before(() => { + projectPermitData = new GetPermitData((null as unknown) as any[]); + }); + + it('sets permits', function () { + expect(projectPermitData.permits).to.eql([]); + }); + }); + + describe('All values provided', () => { + let projectPermitData: GetPermitData; + + const permits = [ + { + number: '1', + type: 'permit type' + } + ]; + + before(() => { + projectPermitData = new GetPermitData(permits); + }); + + it('sets permits', function () { + expect(projectPermitData.permits).to.eql([ + { + permit_number: '1', + permit_type: 'permit type' + } + ]); + }); + }); +}); + describe('GetLocationData', () => { describe('No values provided', () => { let locationData: GetLocationData; @@ -334,116 +306,198 @@ describe('GetLocationData', () => { }); }); -describe('GetProjectData', () => { +describe('GetIUCNClassificationData', () => { describe('No values provided', () => { - let data: GetProjectData; + let iucnClassificationData: GetIUCNClassificationData; before(() => { - data = new GetProjectData(); + iucnClassificationData = new GetIUCNClassificationData((null as unknown) as any[]); }); - it('sets name', () => { - expect(data.project_name).to.equal(''); + it('sets classification details', function () { + expect(iucnClassificationData.classificationDetails).to.eql([]); }); + }); - it('sets type', () => { - expect(data.project_type).to.equal(''); + describe('Empty array as values provided', () => { + let iucnClassificationData: GetIUCNClassificationData; + + before(() => { + iucnClassificationData = new GetIUCNClassificationData([]); }); - it('sets project_activities', () => { - expect(data.project_activities).to.eql([]); + it('sets classification details', function () { + expect(iucnClassificationData.classificationDetails).to.eql([]); }); + }); - it('sets start_date', () => { - expect(data.start_date).to.equal(''); + describe('All values provided', () => { + let iucnClassificationData: GetIUCNClassificationData; + + const iucnClassificationDataObj = [ + { + classification: 'class', + subclassification1: 'subclass1', + subclassification2: 'subclass2' + } + ]; + + before(() => { + iucnClassificationData = new GetIUCNClassificationData(iucnClassificationDataObj); }); - it('sets end_date', () => { - expect(data.end_date).to.equal(''); + it('sets classification details', function () { + expect(iucnClassificationData.classificationDetails).to.eql([ + { + classification: 'class', + subClassification1: 'subclass1', + subClassification2: 'subclass2' + } + ]); }); + }); +}); - it('sets completion_status', () => { - expect(data.completion_status).to.equal(COMPLETION_STATUS.ACTIVE); +describe('GetFundingData', () => { + describe('No values provided', () => { + let projectFundingData: GetFundingData; + + before(() => { + projectFundingData = new GetFundingData((null as unknown) as any[]); + }); + + it('sets permits', function () { + expect(projectFundingData.fundingSources).to.eql([]); }); }); - describe('all values provided', () => { - const projectData = { - name: 'project name', - type: 4, - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', - revision_count: 1 - }; + describe('Empty array as values provided', () => { + let projectFundingData: GetFundingData; - const activityData = [{ activity_id: 1 }, { activity_id: 2 }]; + before(() => { + projectFundingData = new GetFundingData([]); + }); - let data: GetProjectData; + it('sets classification details', function () { + expect(projectFundingData.fundingSources).to.eql([]); + }); + }); + + describe('All values provided', () => { + let projectFundingData: GetFundingData; + + const fundings = [ + { + id: 1, + agency_id: 2, + investment_action_category: 3, + investment_action_category_name: 'Something', + agency_name: 'fake', + funding_amount: 123456, + start_date: Date.now().toString(), + end_date: Date.now().toString(), + agency_project_id: '12', + revision_count: 1 + } + ]; before(() => { - data = new GetProjectData(projectData, activityData); + projectFundingData = new GetFundingData(fundings); }); - it('sets name', () => { - expect(data.project_name).to.equal(projectData.name); + it('sets permits', function () { + expect(projectFundingData.fundingSources).to.eql(fundings); }); + }); +}); - it('sets type', () => { - expect(data.project_type).to.equal(projectData.type); +describe('GetPartnershipsData', () => { + describe('No values provided', () => { + let data: GetPartnershipsData; + + before(() => { + data = new GetPartnershipsData((null as unknown) as any[], (null as unknown) as any[]); }); - it('sets project_activities', () => { - expect(data.project_activities).to.eql([1, 2]); + it('sets indigenous_partnerships', function () { + expect(data.indigenous_partnerships).to.eql([]); }); - it('sets start_date', () => { - expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); + it('sets stakeholder_partnerships', function () { + expect(data.stakeholder_partnerships).to.eql([]); }); + }); - it('sets end_date', () => { - expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); + describe('Empty arrays as values provided', () => { + let data: GetPartnershipsData; + + before(() => { + data = new GetPartnershipsData([], []); }); - it('sets completion_status', () => { - expect(data.completion_status).to.equal(COMPLETION_STATUS.COMPLETED); + it('sets indigenous_partnerships', function () { + expect(data.indigenous_partnerships).to.eql([]); + }); + + it('sets stakeholder_partnerships', function () { + expect(data.stakeholder_partnerships).to.eql([]); }); }); -}); -describe('GetPermitData', () => { - describe('No values provided', () => { - let projectPermitData: GetPermitData; + describe('indigenous_partnerships values provided', () => { + let data: GetPartnershipsData; + + const indigenous_partnerships = [{ id: 1 }, { id: 2 }]; + const stakeholder_partnerships: string[] = []; before(() => { - projectPermitData = new GetPermitData((null as unknown) as any[]); + data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); }); - it('sets permits', function () { - expect(projectPermitData.permits).to.eql([]); + it('sets indigenous_partnerships', function () { + expect(data.indigenous_partnerships).to.eql([1, 2]); + }); + + it('sets stakeholder_partnerships', function () { + expect(data.stakeholder_partnerships).to.eql([]); + }); + }); + + describe('stakeholder_partnerships values provided', () => { + let data: GetPartnershipsData; + + const indigenous_partnerships: number[] = []; + const stakeholder_partnerships = [{ partnership_name: 'partner 1' }, { partnership_name: 'partner 2' }]; + + before(() => { + data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); + }); + + it('sets indigenous_partnerships', function () { + expect(data.indigenous_partnerships).to.eql([]); + }); + + it('sets stakeholder_partnerships', function () { + expect(data.stakeholder_partnerships).to.eql(['partner 1', 'partner 2']); }); }); describe('All values provided', () => { - let projectPermitData: GetPermitData; + let data: GetPartnershipsData; - const permits = [ - { - number: '1', - type: 'permit type' - } - ]; + const indigenous_partnerships = [{ id: 1 }, { id: 2 }]; + const stakeholder_partnerships = [{ partnership_name: 'partner 3' }, { partnership_name: 'partner 4' }]; before(() => { - projectPermitData = new GetPermitData(permits); + data = new GetPartnershipsData(indigenous_partnerships, stakeholder_partnerships); }); - it('sets permits', function () { - expect(projectPermitData.permits).to.eql([ - { - permit_number: '1', - permit_type: 'permit type' - } - ]); + it('sets indigenous_partnerships', function () { + expect(data.indigenous_partnerships).to.eql([1, 2]); + }); + + it('sets stakeholder_partnerships', function () { + expect(data.stakeholder_partnerships).to.eql(['partner 3', 'partner 4']); }); }); }); diff --git a/api/src/models/project-view.ts b/api/src/models/project-view.ts index 668961952d..76a8afb13d 100644 --- a/api/src/models/project-view.ts +++ b/api/src/models/project-view.ts @@ -1,10 +1,22 @@ -import { COMPLETION_STATUS } from '../constants/status'; import { Feature } from 'geojson'; import moment from 'moment'; +import { COMPLETION_STATUS } from '../constants/status'; import { getLogger } from '../utils/logger'; const defaultLog = getLogger('models/project-view'); +export interface IGetProject { + id: number; + coordinator: GetCoordinatorData | null; + permit: GetPermitData | null; + project: GetProjectData | null; + objectives: GetObjectivesData | null; + location: GetLocationData | null; + iucn: GetIUCNClassificationData | null; + funding: GetFundingData | null; + partnerships: GetPartnershipsData | null; +} + /** * Pre-processes GET /projects/{id} project data * @@ -13,13 +25,14 @@ const defaultLog = getLogger('models/project-view'); */ export class GetProjectData { project_name: string; - project_type: string; + project_type: number; project_activities: number[]; start_date: string; end_date: string; comments: string; completion_status: string; publish_date: string; + revision_count: number; constructor(projectData?: any, activityData?: any[]) { defaultLog.debug({ @@ -30,7 +43,7 @@ export class GetProjectData { }); this.project_name = projectData?.name || ''; - this.project_type = projectData?.type || ''; + this.project_type = projectData?.pt_id || -1; this.project_activities = (activityData?.length && activityData.map((item) => item.activity_id)) || []; this.start_date = projectData?.start_date || ''; this.end_date = projectData?.end_date || ''; @@ -41,7 +54,62 @@ export class GetProjectData { moment(projectData.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || COMPLETION_STATUS.ACTIVE; - this.publish_date = projectData?.publish_date || ''; + this.publish_date = String(projectData?.publish_date || ''); + this.revision_count = projectData?.revision_count ?? null; + } +} + +/** + * Pre-processes GET /projects/{id} objectives data + * + * @export + * @class GetObjectivesData + */ +export class GetObjectivesData { + objectives: string; + caveats: string; + revision_count: number; + + constructor(objectivesData?: any) { + defaultLog.debug({ + label: 'GetObjectivesData', + message: 'params', + objectivesData: { ...objectivesData, geometry: 'Too big to print' } + }); + + this.objectives = objectivesData?.objectives || ''; + this.caveats = objectivesData?.caveats || ''; + this.revision_count = objectivesData?.revision_count ?? null; + } +} + +/** + * Pre-processes GET /projects/{id} coordinator data + * + * @export + * @class GetCoordinatorData + */ +export class GetCoordinatorData { + first_name: string; + last_name: string; + email_address: string; + coordinator_agency: string; + share_contact_details: string; + revision_count: number; + + constructor(coordinatorData?: any) { + defaultLog.debug({ + label: 'GetCoordinatorData', + message: 'params', + coordinatorData: { ...coordinatorData, geometry: 'Too big to print' } + }); + + this.first_name = coordinatorData?.coordinator_first_name || ''; + this.last_name = coordinatorData?.coordinator_last_name || ''; + this.email_address = coordinatorData?.coordinator_email_address || ''; + this.coordinator_agency = coordinatorData?.coordinator_agency_name || ''; + this.share_contact_details = coordinatorData?.coordinator_public ? 'true' : 'false'; + this.revision_count = coordinatorData?.revision_count ?? null; } } @@ -87,6 +155,7 @@ export class GetPermitData { export class GetLocationData { location_description: string; geometry?: Feature[]; + revision_count: number; constructor(locationData?: any) { defaultLog.debug({ @@ -101,63 +170,14 @@ export class GetLocationData { this.location_description = locationDataItem?.location_description || ''; this.geometry = (locationDataItem?.geometry?.length && locationDataItem.geometry) || []; - } -} - -/** - * Pre-processes GET /projects/{id} objectives data - * - * @export - * @class GetObjectivesData - */ -export class GetObjectivesData { - objectives: string; - caveats: string; - - constructor(objectivesData?: any) { - defaultLog.debug({ - label: 'GetObjectivesData', - message: 'params', - objectivesData: { ...objectivesData, geometry: 'Too big to print' } - }); - - this.objectives = objectivesData?.objectives || ''; - this.caveats = objectivesData?.caveats || ''; - } -} - -/** - * Pre-processes GET /projects/{id} coordinator data - * - * @export - * @class GetCoordinatorData - */ -export class GetCoordinatorData { - first_name: string; - last_name: string; - email_address: string; - coordinator_agency: string; - share_contact_details: string; - - constructor(coordinatorData?: any) { - defaultLog.debug({ - label: 'GetCoordinatorData', - message: 'params', - coordinatorData: { ...coordinatorData, geometry: 'Too big to print' } - }); - - this.first_name = coordinatorData?.coordinator_first_name || ''; - this.last_name = coordinatorData?.coordinator_last_name || ''; - this.email_address = coordinatorData?.coordinator_email_address || ''; - this.coordinator_agency = coordinatorData?.coordinator_agency_name || ''; - this.share_contact_details = coordinatorData?.coordinator_public ? 'true' : 'false'; + this.revision_count = locationDataItem?.revision_count ?? null; } } interface IGetIUCN { - classification: string; - subClassification1: string; - subClassification2: string; + classification: number; + subClassification1: number; + subClassification2: number; } /** @@ -189,6 +209,49 @@ export class GetIUCNClassificationData { } } +interface IGetFundingSource { + id: number; + agency_id: number; + investment_action_category: number; + investment_action_category_name: string; + agency_name: string; + funding_amount: number; + start_date: string; + end_date: string; + agency_project_id: string; + revision_count: number; +} + +export class GetFundingData { + fundingSources: IGetFundingSource[]; + + constructor(fundingData?: any[]) { + defaultLog.debug({ + label: 'GetFundingData', + message: 'params', + fundingData: fundingData + }); + + this.fundingSources = + (fundingData && + fundingData.map((item: any) => { + return { + id: item.id, + agency_id: item.agency_id, + investment_action_category: item.investment_action_category, + investment_action_category_name: item.investment_action_category_name, + agency_name: item.agency_name, + funding_amount: item.funding_amount, + start_date: item.start_date, + end_date: item.end_date, + agency_project_id: item.agency_project_id, + revision_count: item.revision_count + }; + })) || + []; + } +} + /** * Pre-processes GET /projects/{id} partnerships data * @@ -196,7 +259,7 @@ export class GetIUCNClassificationData { * @class GetPartnershipsData */ export class GetPartnershipsData { - indigenous_partnerships: string[]; + indigenous_partnerships: number[]; stakeholder_partnerships: string[]; constructor(indigenous_partnerships?: any[], stakeholder_partnerships?: any[]) { @@ -208,8 +271,23 @@ export class GetPartnershipsData { }); this.indigenous_partnerships = - (indigenous_partnerships?.length && indigenous_partnerships.map((item: any) => item.fn_name)) || []; + (indigenous_partnerships?.length && indigenous_partnerships.map((item: any) => item.id)) || []; this.stakeholder_partnerships = - (stakeholder_partnerships?.length && stakeholder_partnerships.map((item: any) => item.sp_name)) || []; + (stakeholder_partnerships?.length && stakeholder_partnerships.map((item: any) => item.partnership_name)) || []; + } +} + +export class GetSpeciesData { + focal_species: number[]; + focal_species_names: string[]; + + constructor(input?: any[]) { + this.focal_species = []; + this.focal_species_names = []; + input?.length && + input.forEach((item: any) => { + this.focal_species.push(Number(item.id)); + this.focal_species_names.push(item.label); + }); } } diff --git a/api/src/models/public/project.test.ts b/api/src/models/public/project.test.ts index 82ddcfcb6a..bc47b0c2e5 100644 --- a/api/src/models/public/project.test.ts +++ b/api/src/models/public/project.test.ts @@ -1,83 +1,6 @@ import { expect } from 'chai'; -import { COMPLETION_STATUS } from '../../constants/status'; -import { GetPublicCoordinatorData, GetPublicProjectData } from './project'; import { describe } from 'mocha'; - -describe('GetPublicProjectData', () => { - describe('No values provided', () => { - let data: GetPublicProjectData; - - before(() => { - data = new GetPublicProjectData(); - }); - - it('sets name', () => { - expect(data.project_name).to.equal(''); - }); - - it('sets type', () => { - expect(data.project_type).to.equal(''); - }); - - it('sets project_activities', () => { - expect(data.project_activities).to.eql([]); - }); - - it('sets start_date', () => { - expect(data.start_date).to.equal(''); - }); - - it('sets end_date', () => { - expect(data.end_date).to.equal(''); - }); - - it('sets completion_status', () => { - expect(data.completion_status).to.equal(COMPLETION_STATUS.ACTIVE); - }); - }); - - describe('all values provided', () => { - const projectData = { - name: 'project name', - type: 'type', - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', - revision_count: 1 - }; - - const activityData = [{ name: 'activity1' }, { name: 'activity2' }]; - - let data: GetPublicProjectData; - - before(() => { - data = new GetPublicProjectData(projectData, activityData); - }); - - it('sets name', () => { - expect(data.project_name).to.equal(projectData.name); - }); - - it('sets type', () => { - expect(data.project_type).to.equal(projectData.type); - }); - - it('sets project_activities', () => { - expect(data.project_activities).to.eql(['activity1', 'activity2']); - }); - - it('sets start_date', () => { - expect(data.start_date).to.equal('2020-04-20T07:00:00.000Z'); - }); - - it('sets end_date', () => { - expect(data.end_date).to.equal('2020-05-20T07:00:00.000Z'); - }); - - it('sets completion_status', () => { - expect(data.completion_status).to.equal(COMPLETION_STATUS.COMPLETED); - }); - }); -}); +import { GetPublicCoordinatorData } from './project'; describe('GetPublicCoordinatorData', () => { describe('No values provided', () => { diff --git a/api/src/models/public/project.ts b/api/src/models/public/project.ts index d3ab161c27..7f3f9c20d6 100644 --- a/api/src/models/public/project.ts +++ b/api/src/models/public/project.ts @@ -1,44 +1,7 @@ -import { COMPLETION_STATUS } from '../../constants/status'; -import moment from 'moment'; import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('models/public/project'); -/** - * Pre-processes GET /projects/{id} public (published) project data - * - * @export - * @class GetPublicProjectData - */ -export class GetPublicProjectData { - project_name: string; - project_type: string; - project_activities: string[]; - start_date: string; - end_date: string; - comments: string; - completion_status: string; - publish_date: string; - - constructor(projectData?: any, activityData?: any[]) { - defaultLog.debug({ label: 'GetPublicProjectData', message: 'params', projectData, activityData }); - - this.project_name = projectData?.name || ''; - this.project_type = projectData?.type || ''; - this.project_activities = (activityData?.length && activityData.map((item) => item.name)) || []; - this.start_date = projectData?.start_date || ''; - this.end_date = projectData?.end_date || ''; - this.comments = projectData?.comments || ''; - this.completion_status = - (projectData && - projectData.end_date && - moment(projectData.end_date).endOf('day').isBefore(moment()) && - COMPLETION_STATUS.COMPLETED) || - COMPLETION_STATUS.ACTIVE; - this.publish_date = projectData?.publish_date || ''; - } -} - /** * Pre-processes GET /projects/{id} coordinator data for public (published) projects * @@ -84,9 +47,9 @@ export class GetPublicAttachmentsData { id: item.id, fileName: item.file_name, fileType: item.file_type || 'Report', - lastModified: item.update_date || item.create_date, + lastModified: (item.update_date || item.create_date).toString(), size: item.file_size, - securityToken: item.is_secured + securityToken: item.is_secured ? 'true' : 'false' }; })) || []; diff --git a/api/src/models/survey-create.test.ts b/api/src/models/survey-create.test.ts index bb555e5371..4702836ec4 100644 --- a/api/src/models/survey-create.test.ts +++ b/api/src/models/survey-create.test.ts @@ -14,10 +14,6 @@ describe('PostSurveyObject', () => { expect(data.survey_name).to.equal(null); }); - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(null); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql([]); }); @@ -26,10 +22,6 @@ describe('PostSurveyObject', () => { expect(data.ancillary_species).to.eql([]); }); - it('sets common survey methodology id', () => { - expect(data.common_survey_methodology_id).to.equal(null); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(null); }); @@ -54,6 +46,10 @@ describe('PostSurveyObject', () => { expect(data.foippa_requirements_accepted).to.equal(false); }); + it('sets sedis_procedures_accepted', () => { + expect(data.sedis_procedures_accepted).to.equal(false); + }); + it('sets survey_data_proprietary', () => { expect(data.survey_data_proprietary).to.equal(false); }); @@ -61,6 +57,10 @@ describe('PostSurveyObject', () => { it('sets survey_proprietor', () => { expect(data.survey_proprietor).to.equal(undefined); }); + + it('sets surveyed_all_areas', () => { + expect(data.surveyed_all_areas).to.equal(false); + }); }); describe('All values provided with survey data proprietary is false', () => { @@ -71,6 +71,7 @@ describe('PostSurveyObject', () => { biologist_last_name: 'last', end_date: '2020/04/04', foippa_requirements_accepted: 'true', + sedis_procedures_accepted: 'true', focal_species: [1, 2], ancillary_species: [3, 4], common_survey_methodology_id: 1, @@ -100,7 +101,8 @@ describe('PostSurveyObject', () => { first_nations_id: null, category_rationale: null, proprietor_name: null, - data_sharing_agreement_required: 'false' + data_sharing_agreement_required: 'false', + surveyed_all_areas: 'true' }; before(() => { @@ -111,10 +113,6 @@ describe('PostSurveyObject', () => { expect(data.survey_name).to.equal(surveyObj.survey_name); }); - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(surveyObj.survey_purpose); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql(surveyObj.focal_species); }); @@ -123,10 +121,6 @@ describe('PostSurveyObject', () => { expect(data.ancillary_species).to.eql(surveyObj.ancillary_species); }); - it('sets common_survey_methodology_id', () => { - expect(data.common_survey_methodology_id).to.eql(surveyObj.common_survey_methodology_id); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(surveyObj.start_date); }); @@ -151,6 +145,10 @@ describe('PostSurveyObject', () => { expect(data.foippa_requirements_accepted).to.equal(true); }); + it('sets sedis_procedures_accepted', () => { + expect(data.sedis_procedures_accepted).to.equal(true); + }); + it('sets survey_data_proprietary', () => { expect(data.survey_data_proprietary).to.equal(false); }); @@ -162,6 +160,10 @@ describe('PostSurveyObject', () => { it('sets the geometry', () => { expect(data.geometry).to.eql(surveyObj.geometry); }); + + it('sets surveyed_all_areas', () => { + expect(data.surveyed_all_areas).to.equal(true); + }); }); describe('All values provided with survey data proprietary is true', () => { @@ -172,6 +174,7 @@ describe('PostSurveyObject', () => { biologist_last_name: 'last', end_date: '2020/04/04', foippa_requirements_accepted: 'true', + sedis_procedures_accepted: 'true', focal_species: [1, 2], ancillary_species: [3, 4], common_survey_methodology_id: 1, @@ -184,7 +187,8 @@ describe('PostSurveyObject', () => { first_nations_id: null, category_rationale: 'rationale', proprietor_name: 'name', - data_sharing_agreement_required: 'true' + data_sharing_agreement_required: 'true', + surveyed_all_areas: 'true' }; before(() => { @@ -195,10 +199,6 @@ describe('PostSurveyObject', () => { expect(data.survey_name).to.equal(surveyObj.survey_name); }); - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(surveyObj.survey_purpose); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql(surveyObj.focal_species); }); @@ -207,10 +207,6 @@ describe('PostSurveyObject', () => { expect(data.ancillary_species).to.eql(surveyObj.ancillary_species); }); - it('sets common_survey_methodology_id', () => { - expect(data.common_survey_methodology_id).to.eql(surveyObj.common_survey_methodology_id); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(surveyObj.start_date); }); @@ -235,6 +231,10 @@ describe('PostSurveyObject', () => { expect(data.foippa_requirements_accepted).to.equal(true); }); + it('sets sedis_procedures_accepted', () => { + expect(data.sedis_procedures_accepted).to.equal(true); + }); + it('sets survey_data_proprietary', () => { expect(data.survey_data_proprietary).to.equal(true); }); @@ -248,6 +248,10 @@ describe('PostSurveyObject', () => { disa_required: true }); }); + + it('sets surveyed_all_areas', () => { + expect(data.surveyed_all_areas).to.equal(true); + }); }); }); diff --git a/api/src/models/survey-create.ts b/api/src/models/survey-create.ts index 4c47aeb07a..fe28051811 100644 --- a/api/src/models/survey-create.ts +++ b/api/src/models/survey-create.ts @@ -14,14 +14,19 @@ export class PostSurveyObject { biologist_first_name: string; biologist_last_name: string; foippa_requirements_accepted: boolean; + sedis_procedures_accepted: boolean; focal_species: number[]; ancillary_species: number[]; - common_survey_methodology_id: number; + field_method_id: number; + ecological_season_id: number; + vantage_code_ids: number[]; + surveyed_all_areas: boolean; start_date: string; end_date: string; survey_area_name: string; survey_data_proprietary: boolean; - survey_purpose: string; + intended_outcome_id: number; + additional_details: string; geometry: Feature[]; permit_number: string; permit_type: string; @@ -44,9 +49,10 @@ export class PostSurveyObject { this.biologist_last_name = obj?.biologist_last_name || null; this.end_date = obj?.end_date || null; this.foippa_requirements_accepted = obj?.foippa_requirements_accepted === 'true' || false; + this.sedis_procedures_accepted = obj?.sedis_procedures_accepted === 'true' || false; this.focal_species = (obj?.focal_species?.length && obj.focal_species) || []; this.ancillary_species = (obj?.ancillary_species?.length && obj.ancillary_species) || []; - this.common_survey_methodology_id = obj?.common_survey_methodology_id || null; + this.field_method_id = obj?.field_method_id || null; this.start_date = obj?.start_date || null; this.survey_area_name = obj?.survey_area_name || null; this.permit_number = obj?.permit_number || null; @@ -54,7 +60,11 @@ export class PostSurveyObject { this.funding_sources = (obj?.funding_sources?.length && obj.funding_sources) || []; this.survey_data_proprietary = obj?.survey_data_proprietary === 'true' || false; this.survey_name = obj?.survey_name || null; - this.survey_purpose = obj?.survey_purpose || null; + this.intended_outcome_id = obj?.intended_outcome_id || null; + this.ecological_season_id = obj?.ecological_season_id || null; + this.additional_details = obj?.additional_details || null; + this.vantage_code_ids = (obj?.vantage_code_ids?.length && obj.vantage_code_ids) || []; + this.surveyed_all_areas = obj?.surveyed_all_areas === 'true' || false; this.geometry = (obj?.geometry?.length && obj.geometry) || []; this.survey_proprietor = (obj && obj.survey_data_proprietary === 'true' && new PostSurveyProprietorData(obj)) || undefined; diff --git a/api/src/models/survey-update.test.ts b/api/src/models/survey-update.test.ts index f91a818258..066eb2d92c 100644 --- a/api/src/models/survey-update.test.ts +++ b/api/src/models/survey-update.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { PutSurveyDetailsData, GetUpdateSurveyDetailsData } from './survey-update'; +import { GetUpdateSurveyDetailsData, PutSurveyDetailsData, PutSurveyPurposeAndMethodologyData } from './survey-update'; describe('GetUpdateSurveyDetailsData', () => { describe('No values provided', () => { @@ -14,10 +14,6 @@ describe('GetUpdateSurveyDetailsData', () => { expect(data.survey_name).to.equal(''); }); - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(''); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql([]); }); @@ -26,10 +22,6 @@ describe('GetUpdateSurveyDetailsData', () => { expect(data.ancillary_species).to.eql([]); }); - it('sets common survey methodology id', () => { - expect(data.common_survey_methodology_id).to.equal(null); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(''); }); @@ -70,200 +62,178 @@ describe('GetUpdateSurveyDetailsData', () => { describe('all values provided with species as strings', () => { let data: GetUpdateSurveyDetailsData; - const surveyData = [ - { - id: 1, - name: 'survey name', - objectives: 'purpose of survey', - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', - revision_count: 1, - focal_species: 'species', - ancillary_species: 'ancillary', - common_survey_methodology_id: 1, - lead_first_name: 'lead', - lead_last_name: 'last', - location_name: 'area', - type: 'scientific', - number: '123', - pfs_id: 1, - geometry: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [125.6, 10.1] - }, - properties: { - name: 'Dinagat Islands' - } + const surveyData = { + id: 1, + name: 'survey name', + objectives: 'purpose of survey', + start_date: '2020-04-20T07:00:00.000Z', + end_date: '2020-05-20T07:00:00.000Z', + revision_count: 1, + focal_species: ['species'], + ancillary_species: ['ancillary'], + common_survey_methodology_id: 1, + lead_first_name: 'lead', + lead_last_name: 'last', + location_name: 'area', + type: 'scientific', + number: '123', + pfs_id: [1], + geometry: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [125.6, 10.1] + }, + properties: { + name: 'Dinagat Islands' } - ] - } - ]; - + } + ] + }; before(() => { data = new GetUpdateSurveyDetailsData(surveyData); }); it('sets survey_name', () => { - expect(data.survey_name).to.equal(surveyData[0].name); - }); - - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(surveyData[0].objectives); + expect(data.survey_name).to.equal(surveyData.name); }); it('sets focal_species', () => { - expect(data.focal_species).to.eql([surveyData[0].focal_species]); + expect(data.focal_species).to.eql(surveyData.focal_species); }); it('sets ancillary_species', () => { - expect(data.ancillary_species).to.eql([surveyData[0].ancillary_species]); - }); - - it('sets common survey methodology id', () => { - expect(data.common_survey_methodology_id).to.equal(surveyData[0].common_survey_methodology_id); + expect(data.ancillary_species).to.eql(surveyData.ancillary_species); }); it('sets start_date', () => { - expect(data.start_date).to.equal(surveyData[0].start_date); + expect(data.start_date).to.equal(surveyData.start_date); }); it('sets end_date', () => { - expect(data.end_date).to.equal(surveyData[0].end_date); + expect(data.end_date).to.equal(surveyData.end_date); }); it('sets biologist_first_name', () => { - expect(data.biologist_first_name).to.equal(surveyData[0].lead_first_name); + expect(data.biologist_first_name).to.equal(surveyData.lead_first_name); }); it('sets biologist_last_name', () => { - expect(data.biologist_last_name).to.equal(surveyData[0].lead_last_name); + expect(data.biologist_last_name).to.equal(surveyData.lead_last_name); }); it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(surveyData[0].location_name); + expect(data.survey_area_name).to.equal(surveyData.location_name); }); it('sets revision_count', () => { - expect(data.revision_count).to.equal(surveyData[0].revision_count); + expect(data.revision_count).to.equal(surveyData.revision_count); }); it('sets the geometry', () => { - expect(data.geometry).to.eql(surveyData[0].geometry); + expect(data.geometry).to.eql(surveyData.geometry); }); it('sets permit number', () => { - expect(data.permit_number).to.equal(surveyData[0].number); + expect(data.permit_number).to.equal(surveyData.number); }); it('sets permit type', () => { - expect(data.permit_type).to.equal(surveyData[0].type); + expect(data.permit_type).to.equal(surveyData.type); }); it('sets funding sources', () => { - expect(data.funding_sources).to.eql([1]); + expect(data.funding_sources).to.eql(surveyData.pfs_id); }); }); describe('all values provided with species as numbers', () => { let data: GetUpdateSurveyDetailsData; - const surveyData = [ - { - id: 1, - name: 'survey name', - objectives: 'purpose of survey', - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', - revision_count: 1, - focal_species: 1, - ancillary_species: 2, - common_survey_methodology_id: 1, - lead_first_name: 'lead', - lead_last_name: 'last', - location_name: 'area', - type: 'scientific', - number: '123', - pfs_id: 1, - geometry: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [125.6, 10.1] - }, - properties: { - name: 'Dinagat Islands' - } + const surveyData = { + id: 1, + name: 'survey name', + objectives: 'purpose of survey', + start_date: '2020-04-20T07:00:00.000Z', + end_date: '2020-05-20T07:00:00.000Z', + revision_count: 1, + focal_species: [1], + ancillary_species: [2], + common_survey_methodology_id: 1, + lead_first_name: 'lead', + lead_last_name: 'last', + location_name: 'area', + type: 'scientific', + number: '123', + pfs_id: [1], + geometry: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [125.6, 10.1] + }, + properties: { + name: 'Dinagat Islands' } - ] - } - ]; - + } + ] + }; before(() => { data = new GetUpdateSurveyDetailsData(surveyData); }); it('sets survey_name', () => { - expect(data.survey_name).to.equal(surveyData[0].name); - }); - - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(surveyData[0].objectives); + expect(data.survey_name).to.equal(surveyData.name); }); it('sets focal_species', () => { - expect(data.focal_species).to.eql([surveyData[0].focal_species]); + expect(data.focal_species).to.eql(surveyData.focal_species); }); it('sets ancillary_species', () => { - expect(data.ancillary_species).to.eql([surveyData[0].ancillary_species]); - }); - - it('sets common survey methodology id', () => { - expect(data.common_survey_methodology_id).to.equal(surveyData[0].common_survey_methodology_id); + expect(data.ancillary_species).to.eql(surveyData.ancillary_species); }); it('sets start_date', () => { - expect(data.start_date).to.equal(surveyData[0].start_date); + expect(data.start_date).to.equal(surveyData.start_date); }); it('sets end_date', () => { - expect(data.end_date).to.equal(surveyData[0].end_date); + expect(data.end_date).to.equal(surveyData.end_date); }); it('sets biologist_first_name', () => { - expect(data.biologist_first_name).to.equal(surveyData[0].lead_first_name); + expect(data.biologist_first_name).to.equal(surveyData.lead_first_name); }); it('sets biologist_last_name', () => { - expect(data.biologist_last_name).to.equal(surveyData[0].lead_last_name); + expect(data.biologist_last_name).to.equal(surveyData.lead_last_name); }); it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(surveyData[0].location_name); + expect(data.survey_area_name).to.equal(surveyData.location_name); }); it('sets revision_count', () => { - expect(data.revision_count).to.equal(surveyData[0].revision_count); + expect(data.revision_count).to.equal(surveyData.revision_count); }); it('sets the geometry', () => { - expect(data.geometry).to.eql(surveyData[0].geometry); + expect(data.geometry).to.eql(surveyData.geometry); }); it('sets permit number', () => { - expect(data.permit_number).to.equal(surveyData[0].number); + expect(data.permit_number).to.equal(surveyData.number); }); it('sets permit type', () => { - expect(data.permit_type).to.equal(surveyData[0].type); + expect(data.permit_type).to.equal(surveyData.type); }); it('sets funding sources', () => { - expect(data.funding_sources).to.eql([1]); + expect(data.funding_sources).to.eql(surveyData.pfs_id); }); }); }); @@ -280,10 +250,6 @@ describe('PutSurveyData', () => { expect(data.name).to.equal(null); }); - it('sets objectives', () => { - expect(data.objectives).to.equal(null); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql([]); }); @@ -292,10 +258,6 @@ describe('PutSurveyData', () => { expect(data.ancillary_species).to.eql([]); }); - it('sets common_survey_methodology_id', () => { - expect(data.common_survey_methodology_id).to.equal(null); - }); - it('sets geometry', () => { expect(data.geometry).to.equal(null); }); @@ -369,10 +331,6 @@ describe('PutSurveyData', () => { expect(data.name).to.equal(surveyData.survey_details.survey_name); }); - it('sets objectives', () => { - expect(data.objectives).to.equal(surveyData.survey_details.survey_purpose); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql(surveyData.survey_details.focal_species); }); @@ -381,10 +339,6 @@ describe('PutSurveyData', () => { expect(data.ancillary_species).to.eql(surveyData.survey_details.ancillary_species); }); - it('sets common_survey_methodology_id', () => { - expect(data.common_survey_methodology_id).to.equal(surveyData.survey_details.common_survey_methodology_id); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(surveyData.survey_details.start_date); }); @@ -414,3 +368,96 @@ describe('PutSurveyData', () => { }); }); }); + +describe('PutSurveyPurposeAndMethodologyData', () => { + describe('No values provided', () => { + let data: PutSurveyPurposeAndMethodologyData; + + before(() => { + data = new PutSurveyPurposeAndMethodologyData(null); + }); + + it('sets id', () => { + expect(data.id).to.equal(null); + }); + + it('sets intended_outcomes_id', () => { + expect(data.intended_outcome_id).to.eql(null); + }); + + it('sets field_method_id', () => { + expect(data.field_method_id).to.eql(null); + }); + + it('sets additional_details', () => { + expect(data.additional_details).to.equal(null); + }); + + it('sets ecological_season_id', () => { + expect(data.ecological_season_id).to.equal(null); + }); + + it('sets vantage_code_ids', () => { + expect(data.vantage_code_ids).to.eql([]); + }); + + it('sets surveyed_all_areas', () => { + expect(data.surveyed_all_areas).to.equal(false); + }); + + it('sets revision_count', () => { + expect(data.revision_count).to.equal(null); + }); + }); + + describe('All values provided', () => { + let data: PutSurveyPurposeAndMethodologyData; + + const purposeAndMethodologyData = { + id: 1, + field_method_id: 1, + additional_details: 'additional details', + vantage_code_ids: [1, 2], + ecological_season_id: 1, + intended_outcome_id: 1, + surveyed_all_areas: 'true', + revision_count: 1 + }; + + before(() => { + data = new PutSurveyPurposeAndMethodologyData(purposeAndMethodologyData); + }); + + it('sets id', () => { + expect(data.id).to.equal(purposeAndMethodologyData.id); + }); + + it('sets intended_outcomes_id', () => { + expect(data.intended_outcome_id).to.eql(purposeAndMethodologyData.intended_outcome_id); + }); + + it('sets additional_details', () => { + expect(data.additional_details).to.eql(purposeAndMethodologyData.additional_details); + }); + + it('sets field_method_id', () => { + expect(data.field_method_id).to.equal(purposeAndMethodologyData.field_method_id); + }); + + it('sets ecological_season_id', () => { + expect(data.ecological_season_id).to.equal(purposeAndMethodologyData.ecological_season_id); + }); + + it('sets vantage_code_ids', () => { + expect(data.vantage_code_ids).to.equal(purposeAndMethodologyData.vantage_code_ids); + }); + + it('sets surveyed_all_areas', () => { + expect(data.surveyed_all_areas).to.equal(true); + }); + + it('sets revision_count', () => { + expect(data.revision_count).to.equal(purposeAndMethodologyData.revision_count); + }); + }); +}); diff --git a/api/src/models/survey-update.ts b/api/src/models/survey-update.ts index 926dd15a31..e623ca9e57 100644 --- a/api/src/models/survey-update.ts +++ b/api/src/models/survey-update.ts @@ -1,6 +1,6 @@ -import { COMPLETION_STATUS } from '../constants/status'; import { Feature } from 'geojson'; import moment from 'moment'; +import { COMPLETION_STATUS } from '../constants/status'; import { getLogger } from '../utils/logger'; const defaultLog = getLogger('models/survey-update'); @@ -14,10 +14,8 @@ const defaultLog = getLogger('models/survey-update'); export class GetUpdateSurveyDetailsData { id: number; survey_name: string; - survey_purpose: string; focal_species: (string | number)[]; ancillary_species: (string | number)[]; - common_survey_methodology_id: number; start_date: string; end_date: string; biologist_first_name: string; @@ -43,59 +41,27 @@ export class GetUpdateSurveyDetailsData { } }); - const surveyDataItem = surveyDetailsData && surveyDetailsData.length && surveyDetailsData[0]; - - const focalSpeciesList: string[] = []; - const seenFocalSpecies: string[] = []; - - const ancillarySpeciesList: string[] = []; - const seenAncillarySpecies: string[] = []; - - const fundingSourcesList: number[] = []; - const seenFundingSourceIds: number[] = []; - - surveyDetailsData && - surveyDetailsData.map((item: any) => { - if (!seenFundingSourceIds.includes(item.pfs_id)) { - fundingSourcesList.push(item.pfs_id); - } - seenFundingSourceIds.push(item.pfs_id); - - if (!seenFocalSpecies.includes(item.focal_species)) { - focalSpeciesList.push(item.focal_species); - } - seenFocalSpecies.push(item.focal_species); - - if (!seenAncillarySpecies.includes(item.ancillary_species)) { - ancillarySpeciesList.push(item.ancillary_species); - } - seenAncillarySpecies.push(item.ancillary_species); - }); - - this.id = surveyDataItem?.id ?? null; - this.survey_name = surveyDataItem?.name || ''; - this.survey_purpose = surveyDataItem?.objectives || ''; - this.focal_species = (focalSpeciesList.length && focalSpeciesList.filter((item: string | number) => !!item)) || []; - this.ancillary_species = - (ancillarySpeciesList.length && ancillarySpeciesList.filter((item: string | number) => !!item)) || []; - this.start_date = surveyDataItem?.start_date || ''; - this.end_date = surveyDataItem?.end_date || ''; - this.common_survey_methodology_id = surveyDataItem?.common_survey_methodology_id ?? null; - this.biologist_first_name = surveyDataItem?.lead_first_name || ''; - this.biologist_last_name = surveyDataItem?.lead_last_name || ''; - this.survey_area_name = surveyDataItem?.location_name || ''; - this.geometry = (surveyDataItem?.geometry?.length && surveyDataItem.geometry) || []; - this.permit_number = surveyDataItem?.number || ''; - this.permit_type = surveyDataItem?.type || ''; - this.funding_sources = (fundingSourcesList.length && fundingSourcesList.filter((item: number) => !!item)) || []; - this.revision_count = surveyDataItem?.revision_count ?? null; + this.id = surveyDetailsData?.id ?? null; + this.survey_name = surveyDetailsData?.name || ''; + this.focal_species = surveyDetailsData?.focal_species || []; + this.ancillary_species = surveyDetailsData?.ancillary_species || []; + this.start_date = surveyDetailsData?.start_date || ''; + this.end_date = surveyDetailsData?.end_date || ''; + this.biologist_first_name = surveyDetailsData?.lead_first_name || ''; + this.biologist_last_name = surveyDetailsData?.lead_last_name || ''; + this.survey_area_name = surveyDetailsData?.location_name || ''; + this.geometry = (surveyDetailsData?.geometry?.length && surveyDetailsData.geometry) || []; + this.permit_number = surveyDetailsData?.number || ''; + this.permit_type = surveyDetailsData?.type || ''; + this.funding_sources = surveyDetailsData?.pfs_id || []; + this.revision_count = surveyDetailsData?.revision_count ?? null; this.completion_status = - (surveyDataItem && - surveyDataItem.end_date && - moment(surveyDataItem.end_date).endOf('day').isBefore(moment()) && + (surveyDetailsData && + surveyDetailsData.end_date && + moment(surveyDetailsData.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || COMPLETION_STATUS.ACTIVE; - this.publish_date = surveyDataItem?.publish_date || ''; + this.publish_date = String(surveyDetailsData?.publish_date || ''); } } @@ -107,10 +73,8 @@ export class GetUpdateSurveyDetailsData { */ export class PutSurveyDetailsData { name: string; - objectives: string; focal_species: number[]; ancillary_species: number[]; - common_survey_methodology_id: number; start_date: string; end_date: string; lead_first_name: string; @@ -135,13 +99,11 @@ export class PutSurveyDetailsData { }); this.name = obj?.survey_details?.survey_name || null; - this.objectives = obj?.survey_details?.survey_purpose || null; this.focal_species = (obj?.survey_details?.focal_species?.length && obj.survey_details?.focal_species) || []; this.ancillary_species = (obj?.survey_details?.ancillary_species?.length && obj.survey_details?.ancillary_species) || []; this.start_date = obj?.survey_details?.start_date || null; this.end_date = obj?.survey_details?.end_date || null; - this.common_survey_methodology_id = obj?.survey_details?.common_survey_methodology_id || null; this.lead_first_name = obj?.survey_details?.biologist_first_name || null; this.lead_last_name = obj?.survey_details?.biologist_last_name || null; this.location_name = obj?.survey_details?.survey_area_name || null; @@ -182,3 +144,33 @@ export class PutSurveyProprietorData { this.revision_count = obj?.revision_count ?? null; } } + +/** + * Pre-processes PUT /project/{projectId}/survey/{surveyId} survey purpose and methodology data for update + * + * @export + * @class PutSurveyPurposeAndMethodologyData + */ +export class PutSurveyPurposeAndMethodologyData { + id: number; + intended_outcome_id: number; + field_method_id: number; + additional_details: string; + ecological_season_id: number; + vantage_code_ids: number[]; + surveyed_all_areas: boolean; + revision_count: number; + + constructor(obj?: any) { + defaultLog.debug({ label: 'PutSurveyPurposeAndMethodologyData', message: 'params', obj }); + + this.id = obj?.id ?? null; + this.intended_outcome_id = obj?.intended_outcome_id || null; + this.field_method_id = obj?.field_method_id || null; + this.additional_details = obj?.additional_details || null; + this.ecological_season_id = obj?.ecological_season_id || null; + this.vantage_code_ids = (obj?.vantage_code_ids?.length && obj.vantage_code_ids) || []; + this.surveyed_all_areas = obj?.surveyed_all_areas === 'true' || false; + this.revision_count = obj?.revision_count ?? null; + } +} diff --git a/api/src/models/survey-view-update.test.ts b/api/src/models/survey-view-update.test.ts index 7909069de4..114e957f28 100644 --- a/api/src/models/survey-view-update.test.ts +++ b/api/src/models/survey-view-update.test.ts @@ -29,12 +29,17 @@ describe('GetSurveyProprietorData', () => { it('sets data_sharing_agreement_required', () => { expect(data.data_sharing_agreement_required).to.equal('false'); }); + + it('sets survey_data_proprietary', () => { + expect(data.survey_data_proprietary).to.equal('false'); + }); }); describe('All values provided', () => { let data: GetSurveyProprietorData; const proprietorData = { + id: 1, proprietor_type_name: 'type', first_nations_name: 'fn name', category_rationale: 'rationale', @@ -65,5 +70,9 @@ describe('GetSurveyProprietorData', () => { it('sets data_sharing_agreement_required', () => { expect(data.data_sharing_agreement_required).to.equal('true'); }); + + it('sets survey_data_proprietary', () => { + expect(data.survey_data_proprietary).to.equal('true'); + }); }); }); diff --git a/api/src/models/survey-view-update.ts b/api/src/models/survey-view-update.ts index 4f20d5d3f6..87ea63d1aa 100644 --- a/api/src/models/survey-view-update.ts +++ b/api/src/models/survey-view-update.ts @@ -16,8 +16,8 @@ export class GetSurveyProprietorData { first_nations_id: number; category_rationale: string; proprietor_name: string; - survey_data_proprietary: string; data_sharing_agreement_required: string; + survey_data_proprietary: string; revision_count: number; constructor(data?: any) { @@ -34,8 +34,38 @@ export class GetSurveyProprietorData { this.first_nations_id = data?.first_nations_id ?? null; this.category_rationale = data?.category_rationale || ''; this.proprietor_name = data?.proprietor_name || ''; - this.survey_data_proprietary = data?.survey_data_proprietary || 'true'; this.data_sharing_agreement_required = data?.disa_required ? 'true' : 'false'; + this.survey_data_proprietary = (data?.id && 'true') || 'false'; // The existence of a survey proprietor record indicates the survey data is proprietary this.revision_count = data?.revision_count ?? null; } } + +export class GetSurveyPurposeAndMethodologyData { + constructor(responseData?: any) { + defaultLog.debug({ + label: 'GetSurveyPurposeAndMethodologyData', + message: 'params', + data: responseData + }); + + const obj = {}; + + responseData.forEach((item: any) => { + if (!obj[item.id]) { + obj[item.id] = { + id: item.id, + intended_outcome_id: item.intended_outcome_id, + additional_details: item.additional_details, + field_method_id: item.field_method_id, + ecological_season_id: item.ecological_season_id, + revision_count: item.revision_count, + vantage_code_ids: (item.vantage_id && [item.vantage_id]) || [], + surveyed_all_areas: (item.surveyed_all_areas && 'true') || 'false' + }; + } else { + obj[item.id].vantage_code_ids.push(item.vantage_id); + } + }); + return Object.values(obj); + } +} diff --git a/api/src/models/survey-view.test.ts b/api/src/models/survey-view.test.ts index 9f75364844..307d92479e 100644 --- a/api/src/models/survey-view.test.ts +++ b/api/src/models/survey-view.test.ts @@ -14,10 +14,6 @@ describe('GetViewSurveyDetailsData', () => { expect(data.survey_name).to.equal(''); }); - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(''); - }); - it('sets focal_species', () => { expect(data.focal_species).to.eql([]); }); @@ -26,10 +22,6 @@ describe('GetViewSurveyDetailsData', () => { expect(data.ancillary_species).to.eql([]); }); - it('sets common survey methodology', () => { - expect(data.common_survey_methodology).to.equal(''); - }); - it('sets start_date', () => { expect(data.start_date).to.equal(''); }); @@ -70,110 +62,103 @@ describe('GetViewSurveyDetailsData', () => { describe('all values provided', () => { let data: GetViewSurveyDetailsData; - const surveyData = [ - { - id: 1, - name: 'survey name', - objectives: 'purpose of survey', - start_date: '2020-04-20T07:00:00.000Z', - end_date: '2020-05-20T07:00:00.000Z', - revision_count: 1, - focal_species: 'species', - ancillary_species: 'ancillary', - common_survey_methodology: 'method', - lead_first_name: 'lead', - lead_last_name: 'last', - location_name: 'area', - type: 'scientific', - number: '123', - pfs_id: 1, - funding_amount: 100, - agency_name: 'name agency', - funding_start_date: '2020/04/04', - funding_end_date: '2020/05/05', - geometry: [ - { - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [125.6, 10.1] - }, - properties: { - name: 'Dinagat Islands' - } + const surveyData = { + id: 1, + name: 'survey name', + objectives: 'purpose of survey', + start_date: '2020-04-20T07:00:00.000Z', + end_date: '2020-05-20T07:00:00.000Z', + revision_count: 1, + focal_species: ['species'], + ancillary_species: ['ancillary'], + common_survey_methodology: 'method', + lead_first_name: 'lead', + lead_last_name: 'last', + location_name: 'area', + type: 'scientific', + number: '123', + funding_sources: [ + { + pfs_id: 1, + funding_amount: 100, + agency_name: 'name agency', + funding_start_date: '2020/04/04', + funding_end_date: '2020/05/05' + } + ], + geometry: [ + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [125.6, 10.1] + }, + properties: { + name: 'Dinagat Islands' } - ] - } - ]; - + } + ] + }; before(() => { data = new GetViewSurveyDetailsData(surveyData); }); it('sets survey_name', () => { - expect(data.survey_name).to.equal(surveyData[0].name); - }); - - it('sets survey_purpose', () => { - expect(data.survey_purpose).to.equal(surveyData[0].objectives); + expect(data.survey_name).to.equal(surveyData.name); }); it('sets focal_species', () => { - expect(data.focal_species).to.eql([surveyData[0].focal_species]); + expect(data.focal_species).to.eql(surveyData.focal_species); }); it('sets ancillary_species', () => { - expect(data.ancillary_species).to.eql([surveyData[0].ancillary_species]); - }); - - it('sets common survey methodology', () => { - expect(data.common_survey_methodology).to.equal(surveyData[0].common_survey_methodology); + expect(data.ancillary_species).to.eql(surveyData.ancillary_species); }); it('sets start_date', () => { - expect(data.start_date).to.equal(surveyData[0].start_date); + expect(data.start_date).to.equal(surveyData.start_date); }); it('sets end_date', () => { - expect(data.end_date).to.equal(surveyData[0].end_date); + expect(data.end_date).to.equal(surveyData.end_date); }); it('sets biologist_first_name', () => { - expect(data.biologist_first_name).to.equal(surveyData[0].lead_first_name); + expect(data.biologist_first_name).to.equal(surveyData.lead_first_name); }); it('sets biologist_last_name', () => { - expect(data.biologist_last_name).to.equal(surveyData[0].lead_last_name); + expect(data.biologist_last_name).to.equal(surveyData.lead_last_name); }); it('sets survey_area_name', () => { - expect(data.survey_area_name).to.equal(surveyData[0].location_name); + expect(data.survey_area_name).to.equal(surveyData.location_name); }); it('sets revision_count', () => { - expect(data.revision_count).to.equal(surveyData[0].revision_count); + expect(data.revision_count).to.equal(surveyData.revision_count); }); it('sets the geometry', () => { - expect(data.geometry).to.eql(surveyData[0].geometry); + expect(data.geometry).to.eql(surveyData.geometry); }); it('sets permit number', () => { - expect(data.permit_number).to.equal(surveyData[0].number); + expect(data.permit_number).to.equal(surveyData.number); }); it('sets permit type', () => { - expect(data.permit_type).to.equal(surveyData[0].type); + expect(data.permit_type).to.equal(surveyData.type); }); it('sets funding sources', () => { expect(data.funding_sources).to.eql([ { - agency_name: surveyData[0].agency_name, - pfs_id: surveyData[0].pfs_id, - funding_amount: surveyData[0].funding_amount, - funding_start_date: surveyData[0].funding_start_date, - funding_end_date: surveyData[0].funding_end_date + pfs_id: 1, + funding_amount: 100, + agency_name: 'name agency', + funding_start_date: '2020/04/04', + funding_end_date: '2020/05/05' } ]); }); diff --git a/api/src/models/survey-view.ts b/api/src/models/survey-view.ts index 8d9a6074de..84c2c2ba88 100644 --- a/api/src/models/survey-view.ts +++ b/api/src/models/survey-view.ts @@ -1,6 +1,6 @@ -import { COMPLETION_STATUS } from '../constants/status'; import { Feature } from 'geojson'; import moment from 'moment'; +import { COMPLETION_STATUS } from '../constants/status'; import { getLogger } from '../utils/logger'; const defaultLog = getLogger('models/survey-view'); @@ -14,10 +14,10 @@ const defaultLog = getLogger('models/survey-view'); export class GetViewSurveyDetailsData { id: number; survey_name: string; - survey_purpose: string; - focal_species: (string | number)[]; - ancillary_species: (string | number)[]; - common_survey_methodology: string; + focal_species: number[]; + focal_species_names: string[]; + ancillary_species: number[]; + ancillary_species_names: string[]; start_date: string; end_date: string; biologist_first_name: string; @@ -27,11 +27,10 @@ export class GetViewSurveyDetailsData { revision_count: number; permit_number: string; permit_type: string; - funding_sources: any[]; + funding_sources: object[]; completion_status: string; publish_date: string; occurrence_submission_id: number; - summary_results_submission_id: number; constructor(surveyDetailsData?: any) { defaultLog.debug({ @@ -45,105 +44,45 @@ export class GetViewSurveyDetailsData { } }); - const surveyDataItem = surveyDetailsData && surveyDetailsData.length && surveyDetailsData[0]; - - const focalSpeciesList: string[] = []; - const seenFocalSpecies: string[] = []; - - const ancillarySpeciesList: string[] = []; - const seenAncillarySpecies: string[] = []; - - const fundingSourcesList: any[] = []; - const seenFundingSourceIds: number[] = []; - - surveyDetailsData && - surveyDetailsData.map((item: any) => { - if (!seenFundingSourceIds.includes(item.pfs_id) && item.pfs_id) { - fundingSourcesList.push({ - agency_name: item.agency_name, - pfs_id: item.pfs_id, - funding_amount: item.funding_amount, - funding_start_date: item.funding_start_date, - funding_end_date: item.funding_end_date - }); - } - seenFundingSourceIds.push(item.pfs_id); - - if (!seenFocalSpecies.includes(item.focal_species)) { - focalSpeciesList.push(item.focal_species); - } - seenFocalSpecies.push(item.focal_species); - - if (!seenAncillarySpecies.includes(item.ancillary_species)) { - ancillarySpeciesList.push(item.ancillary_species); - } - seenAncillarySpecies.push(item.ancillary_species); - }); - - this.id = surveyDataItem?.id ?? null; - this.occurrence_submission_id = surveyDataItem?.occurrence_submission_id ?? null; - this.summary_results_submission_id = surveyDataItem?.summary_results_submission_id ?? null; - this.survey_name = surveyDataItem?.name || ''; - this.survey_purpose = surveyDataItem?.objectives || ''; - this.focal_species = (focalSpeciesList.length && focalSpeciesList.filter((item: string | number) => !!item)) || []; - this.ancillary_species = - (ancillarySpeciesList.length && ancillarySpeciesList.filter((item: string | number) => !!item)) || []; - this.start_date = surveyDataItem?.start_date || ''; - this.end_date = surveyDataItem?.end_date || ''; - this.biologist_first_name = surveyDataItem?.lead_first_name || ''; - this.common_survey_methodology = surveyDataItem?.common_survey_methodology || ''; - this.biologist_last_name = surveyDataItem?.lead_last_name || ''; - this.survey_area_name = surveyDataItem?.location_name || ''; - this.geometry = (surveyDataItem?.geometry?.length && surveyDataItem.geometry) || []; - this.permit_number = surveyDataItem?.number || ''; - this.permit_type = surveyDataItem?.type || ''; - this.funding_sources = (fundingSourcesList.length && fundingSourcesList.filter((item: any) => !!item)) || []; - this.revision_count = surveyDataItem?.revision_count ?? null; + this.id = surveyDetailsData?.id ?? null; + this.occurrence_submission_id = surveyDetailsData?.occurrence_submission_id ?? null; + this.survey_name = surveyDetailsData?.name || ''; + this.focal_species = surveyDetailsData?.focal_species || []; + this.focal_species_names = surveyDetailsData?.focal_species_names || []; + this.ancillary_species = surveyDetailsData?.ancillary_species || []; + this.ancillary_species_names = surveyDetailsData?.ancillary_species_names || []; + this.start_date = surveyDetailsData?.start_date || ''; + this.end_date = surveyDetailsData?.end_date || ''; + this.biologist_first_name = surveyDetailsData?.lead_first_name || ''; + this.biologist_last_name = surveyDetailsData?.lead_last_name || ''; + this.survey_area_name = surveyDetailsData?.location_name || ''; + this.geometry = (surveyDetailsData?.geometry?.length && surveyDetailsData.geometry) || []; + this.permit_number = surveyDetailsData?.number || ''; + this.permit_type = surveyDetailsData?.type || ''; + this.funding_sources = surveyDetailsData?.funding_sources || []; + this.revision_count = surveyDetailsData?.revision_count ?? null; this.completion_status = - (surveyDataItem && - surveyDataItem.end_date && - moment(surveyDataItem.end_date).endOf('day').isBefore(moment()) && + (surveyDetailsData && + surveyDetailsData.end_date && + moment(surveyDetailsData.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || COMPLETION_STATUS.ACTIVE; - this.publish_date = surveyDataItem?.publish_date || ''; + this.publish_date = String(surveyDetailsData?.publish_date || ''); } } -/** - * Pre-processes GET surveys list data - * - * @export - * @class GetSurveyListData - */ -export class GetSurveyListData { - surveys: any[]; - - constructor(obj?: any) { - defaultLog.debug({ label: 'GetSurveyListData', message: 'params', obj }); - - const surveysList: any[] = []; - const seenSurveyIds: number[] = []; - - obj && - obj.map((survey: any) => { - if (!seenSurveyIds.includes(survey.id)) { - surveysList.push({ - id: survey.id, - name: survey.name, - start_date: survey.start_date, - end_date: survey.end_date, - species: [survey.species], - publish_timestamp: survey.publish_timestamp - }); - } else { - const index = surveysList.findIndex((item) => item.id === survey.id); - surveysList[index].species = [...surveysList[index].species, survey.species]; - } - - seenSurveyIds.push(survey.id); +export class GetSpeciesData { + species: number[]; + species_names: string[]; + + constructor(input?: any[]) { + this.species = []; + this.species_names = []; + input?.length && + input.forEach((item: any) => { + this.species.push(Number(item.id)); + this.species_names.push(item.label); }); - - this.surveys = (surveysList.length && surveysList) || []; } } @@ -175,3 +114,62 @@ export class GetSurveyFundingSources { this.fundingSources = surveyFundingSourcesList; } } + +export class GetFocalSpeciesData { + focal_species: number[]; + focal_species_names: string[]; + + constructor(input?: any[]) { + this.focal_species = []; + this.focal_species_names = []; + + input?.length && + input.forEach((item: any) => { + this.focal_species.push(Number(item.id)); + this.focal_species_names.push(item.label); + }); + } +} + +export class GetAncillarySpeciesData { + ancillary_species: number[]; + ancillary_species_names: string[]; + + constructor(input?: any[]) { + this.ancillary_species = []; + this.ancillary_species_names = []; + + input?.length && + input.forEach((item: any) => { + this.ancillary_species.push(Number(item.id)); + this.ancillary_species_names.push(item.label); + }); + } +} + +export type SurveyObject = { + survey: GetSurveyData; + species: GetSpeciesData; +}; + +export class GetSurveyData { + id: number; + name: string; + start_date: string; + end_date: string; + publish_status: string; + completion_status: number; + + constructor(surveyData?: any) { + this.id = surveyData?.survey_id || null; + this.name = surveyData?.name || ''; + this.start_date = surveyData?.start_date || null; + this.end_date = surveyData?.end_date || null; + this.publish_status = surveyData?.publish_timestamp ? 'Published' : 'Unpublished'; + this.completion_status = + (surveyData.end_date && + moment(surveyData.end_date).endOf('day').isBefore(moment()) && + COMPLETION_STATUS.COMPLETED) || + COMPLETION_STATUS.ACTIVE; + } +} diff --git a/api/src/models/user.test.ts b/api/src/models/user.test.ts index ed42235822..c776cd77f4 100644 --- a/api/src/models/user.test.ts +++ b/api/src/models/user.test.ts @@ -26,7 +26,7 @@ describe('UserObject', () => { describe('valid values provided, no roles', () => { let data: UserObject; - const userObject = { id: 1, user_identifier: 'test name', role_ids: [], role_names: [] }; + const userObject = { system_user_id: 1, user_identifier: 'test name', role_ids: [], role_names: [] }; before(() => { data = new UserObject(userObject); @@ -52,7 +52,12 @@ describe('UserObject', () => { describe('valid values provided', () => { let data: UserObject; - const userObject = { id: 1, user_identifier: 'test name', role_ids: [1, 2], role_names: ['role 1', 'role 2'] }; + const userObject = { + system_user_id: 1, + user_identifier: 'test name', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }; before(() => { data = new UserObject(userObject); diff --git a/api/src/models/user.ts b/api/src/models/user.ts index e54181ce5f..587b1288e7 100644 --- a/api/src/models/user.ts +++ b/api/src/models/user.ts @@ -5,15 +5,33 @@ const defaultLog = getLogger('models/user'); export class UserObject { id: number; user_identifier: string; + record_end_date: string; role_ids: number[]; role_names: string[]; constructor(obj?: any) { defaultLog.debug({ label: 'UserObject', message: 'params', obj }); - this.id = obj?.id || null; + this.id = obj?.system_user_id || null; this.user_identifier = obj?.user_identifier || null; + this.record_end_date = obj?.record_end_date || null; this.role_ids = (obj?.role_ids?.length && obj.role_ids) || []; this.role_names = (obj?.role_names?.length && obj.role_names) || []; } } + +export class ProjectUserObject { + project_id: number; + system_user_id: number; + project_role_ids: number[]; + project_role_names: string[]; + + constructor(obj?: any) { + defaultLog.debug({ label: 'ProjectUserObject', message: 'params', obj }); + + this.project_id = obj?.project_id || null; + this.system_user_id = obj?.system_user_id || null; + this.project_role_ids = (obj?.project_role_ids?.length && obj.project_role_ids) || []; + this.project_role_names = (obj?.project_role_names?.length && obj.project_role_names) || []; + } +} diff --git a/api/src/openapi/root-api-doc.ts b/api/src/openapi/root-api-doc.ts index 752525e2f3..dd9647f543 100644 --- a/api/src/openapi/root-api-doc.ts +++ b/api/src/openapi/root-api-doc.ts @@ -1,22 +1,9 @@ -const getHTTPResponse = (description: string) => { - return { - description, - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Error' - } - } - } - }; -}; - export const rootAPIDoc = { openapi: '3.0.0', info: { version: '0.0.0', - title: 'biohubbc-api', - description: 'API for BioHubBC', + title: 'sims-api', + description: 'API for SIMS (Species Inventory Management System)', license: { name: 'Apache 2.0', url: 'https://www.apache.org/licenses/LICENSE-2.0.html' @@ -27,10 +14,6 @@ export const rootAPIDoc = { url: 'http://localhost:6100/api', description: 'local api via docker' }, - { - url: 'http://localhost:80/api', - description: 'local api via docker via nginx' - }, { url: 'https://api-dev-biohubbc.apps.silver.devops.gov.bc.ca', description: 'deployed api in dev environment' @@ -44,16 +27,7 @@ export const rootAPIDoc = { description: 'deployed api in prod environment' } ], - tags: [ - { - name: 'template', - description: - 'Template information used by the frontends (via RJSF) to automatically generate forms to capture/render semi-structured data', - externalDocs: { - url: 'react-jsonschema-form.readthedocs.io' - } - } - ], + tags: [], externalDocs: { description: 'Visit GitHub to find out more about this API', url: 'https://github.com/bcgov/biohubbc.git' @@ -70,16 +44,71 @@ export const rootAPIDoc = { } }, responses: { - '400': getHTTPResponse('Bad request'), - '401': getHTTPResponse('Unauthenticated user'), - '403': getHTTPResponse('Unauthorized user'), - '409': getHTTPResponse('Conflict'), - '500': getHTTPResponse('Server error'), - default: getHTTPResponse('Unexpected error') + '400': { + description: 'Bad Request', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '401': { + description: 'Unauthorized', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '403': { + description: 'Forbidden', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '409': { + description: 'Conflict', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + '500': { + description: 'Internal Server Error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + }, + default: { + description: 'Unknown Error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + } + } + } + } }, schemas: { Error: { description: 'Error response object', + required: ['name', 'status', 'message'], properties: { name: { type: 'string' diff --git a/api/src/openapi/schemas/administrative-activity.ts b/api/src/openapi/schemas/administrative-activity.ts index bf87fc9b47..6c3cff4245 100644 --- a/api/src/openapi/schemas/administrative-activity.ts +++ b/api/src/openapi/schemas/administrative-activity.ts @@ -10,8 +10,8 @@ export const administrativeActivityResponseObject = { type: 'number' }, date: { - type: 'string', - description: 'The date this administrative activity was made' + description: 'The date this administrative activity was made', + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }] } } }; diff --git a/api/src/openapi/schemas/geoJson.ts b/api/src/openapi/schemas/geoJson.ts new file mode 100644 index 0000000000..b860b07057 --- /dev/null +++ b/api/src/openapi/schemas/geoJson.ts @@ -0,0 +1,437 @@ +/** + * Response object for viewing geoJson + * https://geojson.org/schema/GeoJSON.json + */ + +export const geoJsonFeature = { + title: 'GeoJSON Feature', + type: 'object', + required: ['type', 'properties', 'geometry'], + properties: { + type: { + type: 'string', + enum: ['Feature'] + }, + id: { + oneOf: [ + { + type: 'number' + }, + { + type: 'string' + } + ] + }, + properties: { + oneOf: [ + { + type: 'null' + }, + { + type: 'object' + } + ] + }, + geometry: { + oneOf: [ + { + type: 'null' + }, + { + title: 'GeoJSON Point', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['Point'] + }, + coordinates: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON LineString', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['LineString'] + }, + coordinates: { + type: 'array', + minItems: 2, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON Polygon', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['Polygon'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 4, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiPoint', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiPoint'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiLineString', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiLineString'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 2, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiPolygon', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiPolygon'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + items: { + type: 'array', + minItems: 4, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON GeometryCollection', + type: 'object', + required: ['type', 'geometries'], + properties: { + type: { + type: 'string', + enum: ['GeometryCollection'] + }, + geometries: { + type: 'array', + items: { + oneOf: [ + { + title: 'GeoJSON Point', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['Point'] + }, + coordinates: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON LineString', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['LineString'] + }, + coordinates: { + type: 'array', + minItems: 2, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON Polygon', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['Polygon'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 4, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiPoint', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiPoint'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiLineString', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiLineString'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + minItems: 2, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + }, + { + title: 'GeoJSON MultiPolygon', + type: 'object', + required: ['type', 'coordinates'], + properties: { + type: { + type: 'string', + enum: ['MultiPolygon'] + }, + coordinates: { + type: 'array', + items: { + type: 'array', + items: { + type: 'array', + minItems: 4, + items: { + type: 'array', + minItems: 2, + items: { + type: 'number' + } + } + } + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + } + ] + } + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } + } + ] + }, + bbox: { + type: 'array', + minItems: 4, + items: { + type: 'number' + } + } + } +}; diff --git a/api/src/openapi/schemas/permit-no-sampling.ts b/api/src/openapi/schemas/permit-no-sampling.ts index 8bdbea71a5..e256cdbe93 100644 --- a/api/src/openapi/schemas/permit-no-sampling.ts +++ b/api/src/openapi/schemas/permit-no-sampling.ts @@ -59,7 +59,7 @@ export const permitNoSamplingPostBody = { export const permitNoSamplingResponseBody = { title: 'Permit no sampling Response Object', type: 'object', - required: ['id'], + required: ['ids'], properties: { ids: { type: 'array', diff --git a/api/src/openapi/schemas/project.test.ts b/api/src/openapi/schemas/project.test.ts index 9f39206e91..0573b5a1cf 100644 --- a/api/src/openapi/schemas/project.test.ts +++ b/api/src/openapi/schemas/project.test.ts @@ -5,8 +5,7 @@ import { projectCreatePostRequestObject, projectIdResponseObject, projectUpdateGetResponseObject, - projectUpdatePutRequestObject, - projectViewGetResponseObject + projectUpdatePutRequestObject } from './project'; describe('projectCreatePostRequestObject', () => { @@ -40,11 +39,3 @@ describe('projectUpdatePutRequestObject', () => { expect(ajv.validateSchema(projectUpdatePutRequestObject)).to.be.true; }); }); - -describe('projectViewGetResponseObject', () => { - const ajv = new Ajv(); - - it('is valid openapi v3 schema', () => { - expect(ajv.validateSchema(projectViewGetResponseObject)).to.be.true; - }); -}); diff --git a/api/src/openapi/schemas/project.ts b/api/src/openapi/schemas/project.ts index c2f43644e3..8c48824117 100644 --- a/api/src/openapi/schemas/project.ts +++ b/api/src/openapi/schemas/project.ts @@ -163,15 +163,6 @@ export const projectCreatePostRequestObject = { } }; -/** - * Response object for project view GET request - */ -export const projectViewGetResponseObject = { - title: 'Project get response object, for view purposes', - type: 'object', - properties: {} -}; - const projectUpdateProperties = { coordinator: { type: 'object', diff --git a/api/src/openapi/schemas/survey.test.ts b/api/src/openapi/schemas/survey.test.ts deleted file mode 100644 index 4235a8366c..0000000000 --- a/api/src/openapi/schemas/survey.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Ajv from 'ajv'; -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { surveyCreatePostRequestObject, surveyIdResponseObject } from './survey'; - -describe('surveyCreatePostRequestObject', () => { - const ajv = new Ajv(); - - it('is valid openapi v3 schema', () => { - expect(ajv.validateSchema(surveyCreatePostRequestObject)).to.be.true; - }); -}); - -describe('surveyIdResponseObject', () => { - const ajv = new Ajv(); - - it('is valid openapi v3 schema', () => { - expect(ajv.validateSchema(surveyIdResponseObject)).to.be.true; - }); -}); diff --git a/api/src/openapi/schemas/survey.ts b/api/src/openapi/schemas/survey.ts deleted file mode 100644 index b957162d9b..0000000000 --- a/api/src/openapi/schemas/survey.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Request Object for survey create POST request - */ -export const surveyCreatePostRequestObject = { - title: 'SurveyProject post request object', - type: 'object', - required: [ - 'survey_name', - 'start_date', - 'end_date', - 'focal_species', - 'ancillary_species', - 'survey_purpose', - 'biologist_first_name', - 'biologist_last_name', - 'survey_area_name', - 'survey_data_proprietary' - ], - properties: { - survey_name: { - type: 'string' - }, - start_date: { - type: 'string', - description: 'ISO 8601 date string' - }, - end_date: { - type: 'string', - description: 'ISO 8601 date string' - }, - focal_species: { - type: 'array', - items: { - type: 'number' - }, - description: 'Selected focal species ids' - }, - ancillary_species: { - type: 'array', - items: { - type: 'number' - }, - description: 'Selected ancillary species ids' - }, - survey_purpose: { - type: 'string' - }, - biologist_first_name: { - type: 'string' - }, - biologist_last_name: { - type: 'string' - }, - survey_area_name: { - type: 'string' - }, - survey_data_proprietary: { - type: 'string' - }, - proprietary_data_category: { - type: 'number' - }, - proprietor_name: { - type: 'string' - }, - category_rationale: { - type: 'string' - }, - first_nations_id: { - type: 'number' - }, - data_sharing_agreement_required: { - type: 'string' - } - } -}; - -/** - * Basic response object for a survey. - */ -export const surveyIdResponseObject = { - title: 'Survey Response Object', - type: 'object', - required: ['id'], - properties: { - id: { - type: 'number' - } - } -}; - -/** - * Response object for survey view GET request - */ -export const surveyViewGetResponseObject = { - title: 'Survey get response object, for view purposes', - type: 'object', - properties: {} -}; - -/** - * Response object for survey update GET request - */ -export const surveyUpdateGetResponseObject = { - title: 'Survey get response object, for update purposes', - type: 'object', - properties: {} -}; - -/** - * Request object for survey update PUT request - */ -export const surveyUpdatePutRequestObject = { - title: 'Survey Put Object', - type: 'object', - properties: { - survey_name: { type: 'string' }, - survey_purpose: { type: 'string' }, - focal_species: { - type: 'array', - items: { - type: 'number' - }, - description: 'Selected focal species ids' - }, - ancillary_species: { - type: 'array', - items: { - type: 'number' - }, - description: 'Selected ancillary species ids' - }, - start_date: { type: 'string' }, - end_date: { type: 'string' }, - biologist_first_name: { type: 'string' }, - biologist_last_name: { type: 'string' }, - survey_area_name: { type: 'string' }, - revision_count: { type: 'number' } - } -}; diff --git a/api/src/paths/access-request.test.ts b/api/src/paths/access-request.test.ts deleted file mode 100644 index fb704ffc63..0000000000 --- a/api/src/paths/access-request.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as access_request from './access-request'; -import * as user_queries from '../queries/users/user-queries'; -import * as db from '../database/db'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../__mocks__/db'; - -chai.use(sinonChai); - -describe('updateAccessRequest', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - body: { - userIdentifier: 1, - identitySource: 'identitySource', - requestId: 1, - requestStatusTypeId: 1 - } - } as any; - - it('should throw a 400 error when no user identifier body param', async () => { - try { - const result = access_request.updateAccessRequest(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, userIdentifier: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: userIdentifier'); - } - }); - - it('should throw a 400 error when no identity source body param', async () => { - try { - const result = access_request.updateAccessRequest(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, identitySource: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: identitySource'); - } - }); - - it('should throw a 400 error when no request id body param', async () => { - try { - const result = access_request.updateAccessRequest(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, requestId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: requestId'); - } - }); - - it('should throw a 400 error when no request status type id body param', async () => { - try { - const result = access_request.updateAccessRequest(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, requestStatusTypeId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: requestStatusTypeId'); - } - }); - - it('should throw a 400 error when fails to get getUserByUserIdentifierSQL statement', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - sinon.stub(user_queries, 'getUserByUserIdentifierSQL').returns(null); - - try { - const result = access_request.updateAccessRequest(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 400 error when no userId and no systemUserId', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: [null], - rowCount: 1 - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return null; - }, - query: mockQuery - }); - sinon.stub(user_queries, 'getUserByUserIdentifierSQL').returns(SQL`something`); - - try { - const result = access_request.updateAccessRequest(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to identify system user ID'); - } - }); - - it('should throw a 500 error when userId but no userObject', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: [ - { - id: 1, - user_identifier: null, - role_ids: [1, 2], - role_name: ['System Admin', 'Project Lead'] - } - ], - rowCount: 1 - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return null; - }, - query: mockQuery - }); - sinon.stub(user_queries, 'getUserByUserIdentifierSQL').returns(SQL`something`); - - try { - const result = access_request.updateAccessRequest(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to get or add system user'); - } - }); -}); diff --git a/api/src/paths/access-request.ts b/api/src/paths/access-request.ts deleted file mode 100644 index fc3bc62ade..0000000000 --- a/api/src/paths/access-request.ts +++ /dev/null @@ -1,177 +0,0 @@ -'use strict'; - -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getDBConnection } from '../database/db'; -import { HTTP400, HTTP500 } from '../errors/CustomError'; -import { UserObject } from '../models/user'; -import { getUserByUserIdentifierSQL } from '../queries/users/user-queries'; -import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; -import { updateAdministrativeActivity } from './administrative-activity'; -import { addSystemUser } from './user'; -import { addSystemRoles } from './user/{userId}/system-roles'; - -const defaultLog = getLogger('paths/access-request'); - -export const PUT: Operation = [logRequest('paths/access-request', 'POST'), updateAccessRequest()]; - -PUT.apiDoc = { - description: "Update a user's system access request and add any specified system roles to the user.", - tags: ['user'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - required: ['userIdentifier', 'identitySource', 'requestId', 'requestStatusTypeId'], - properties: { - userIdentifier: { - type: 'string', - description: 'The user identifier for the user.' - }, - identitySource: { - type: 'string', - description: 'The identity source for the user.' - }, - requestId: { - type: 'number', - description: 'The id of the access request to update.' - }, - requestStatusTypeId: { - type: 'number', - description: 'The status type id to set for the access request.' - }, - roleIds: { - type: 'array', - items: { - type: 'number' - }, - description: - 'An array of role ids to add, if the access-request was approved. Ignored if the access-request was denied.' - } - } - } - } - } - }, - responses: { - 200: { - description: 'Add system user roles to user OK.' - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -/** - * Updates an access request. - * - * key steps performed: - * - Get the user by their user identifier - * - If user is not found, add them - * - Determine if there are any new roles to add, and add them if there are - * - Update the administrative activity record status - * - * @return {*} {RequestHandler} - */ -export function updateAccessRequest(): RequestHandler { - return async (req, res) => { - defaultLog.debug({ label: 'updateAccessRequest', message: 'params', req_body: req.body }); - - const userIdentifier = req.body?.userIdentifier || null; - const identitySource = req.body?.identitySource || null; - const administrativeActivityId = Number(req.body?.requestId) || null; - const administrativeActivityStatusTypeId = Number(req.body?.requestStatusTypeId) || null; - const roleIds: number[] = req.body?.roleIds || []; - - if (!userIdentifier) { - throw new HTTP400('Missing required body param: userIdentifier'); - } - - if (!identitySource) { - throw new HTTP400('Missing required body param: identitySource'); - } - - if (!administrativeActivityId) { - throw new HTTP400('Missing required body param: requestId'); - } - - if (!administrativeActivityStatusTypeId) { - throw new HTTP400('Missing required body param: requestStatusTypeId'); - } - - const getUserSQLStatement = getUserByUserIdentifierSQL(userIdentifier); - - if (!getUserSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const connection = getDBConnection(req['keycloak_token']); - - try { - await connection.open(); - - // Get the user by their user identifier (user may not exist) - const getUserResponse = await connection.query(getUserSQLStatement.text, getUserSQLStatement.values); - - let userData = (getUserResponse && getUserResponse.rowCount && getUserResponse.rows[0]) || null; - - if (!userData) { - const systemUserId = connection.systemUserId(); - - if (!systemUserId) { - throw new HTTP400('Failed to identify system user ID'); - } - - // Found no existing user, add them - userData = await addSystemUser(userIdentifier, identitySource, systemUserId, connection); - } - - const userObject = new UserObject(userData); - - if (!userObject.id || !userObject.user_identifier) { - throw new HTTP500('Failed to get or add system user'); - } - - // Filter out any system roles that have already been added to the user - const rolesIdsToAdd = roleIds.filter((roleId) => !userObject.role_ids.includes(roleId)); - - if (rolesIdsToAdd?.length) { - // Add any missing roles (if any) - await addSystemRoles(userObject.id, rolesIdsToAdd, connection); - } - - // Update the access request record status - await updateAdministrativeActivity(administrativeActivityId, administrativeActivityStatusTypeId, connection); - - await connection.commit(); - - return res.status(200).send(); - } catch (error) { - defaultLog.error({ label: 'updateAccessRequest', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} diff --git a/api/src/paths/administrative-activities.test.ts b/api/src/paths/administrative-activities.test.ts index 4f35bdf90d..cbc0ce1525 100644 --- a/api/src/paths/administrative-activities.test.ts +++ b/api/src/paths/administrative-activities.test.ts @@ -2,11 +2,11 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as administrative_activities from './administrative-activities'; -import * as administrative_queries from '../queries/administrative-activity/administrative-activity-queries'; -import * as db from '../database/db'; -import { getMockDBConnection } from '../__mocks__/db'; import SQL from 'sql-template-strings'; +import * as db from '../database/db'; +import administrative_queries from '../queries/administrative-activity'; +import { getMockDBConnection, getRequestHandlerMocks } from '../__mocks__/db'; +import * as administrative_activities from './administrative-activities'; chai.use(sinonChai); @@ -15,65 +15,33 @@ describe('getAdministrativeActivities', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - query: { - type: 'type', - status: ['status'] - } - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when failed to build getAdministrativeActivitiesSQL statement', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(administrative_queries, 'getAdministrativeActivitiesSQL').returns(null); - - try { - const result = administrative_activities.getAdministrativeActivities(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - it('should return the rows on success (empty)', async () => { sinon.stub(administrative_queries, 'getAdministrativeActivitiesSQL').returns(SQL`some`); - const mockQuery = sinon.stub(); - - mockQuery.resolves({ + const mockQuery = sinon.stub().resolves({ rows: null, rowCount: 0 }); - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, query: mockQuery }); + const mockDBConnection = getMockDBConnection({ query: mockQuery }); - const result = administrative_activities.getAdministrativeActivities(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - expect(actualResult).to.eql([]); + mockReq.query = { + type: ['type'], + status: ['status'] + }; + + const requestHandler = administrative_activities.getAdministrativeActivities(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.jsonValue).to.eql([]); }); it('should return the rows on success (not empty)', async () => { - sinon.stub(administrative_queries, 'getAdministrativeActivitiesSQL').returns(SQL`some`); - const data = { id: 1, type: 'type', @@ -86,19 +54,26 @@ describe('getAdministrativeActivities', () => { create_date: '2020/04/04' }; - const mockQuery = sinon.stub(); - - mockQuery.resolves({ + const mockQuery = sinon.stub().resolves({ rows: [data], rowCount: 1 }); - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, query: mockQuery }); + const mockDBConnection = getMockDBConnection({ query: mockQuery }); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.query = { + type: ['type'], + status: ['status'] + }; - const result = administrative_activities.getAdministrativeActivities(); + const requestHandler = administrative_activities.getAdministrativeActivities(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.eql([data]); + expect(mockRes.jsonValue).to.eql([data]); }); }); diff --git a/api/src/paths/administrative-activities.ts b/api/src/paths/administrative-activities.ts index f64a4db095..90a47791c1 100644 --- a/api/src/paths/administrative-activities.ts +++ b/api/src/paths/administrative-activities.ts @@ -2,14 +2,31 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; -import { getAdministrativeActivitiesSQL } from '../queries/administrative-activity/administrative-activity-queries'; +import { queries } from '../queries/queries'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; -const defaultLog = getLogger('paths/administrative-activity'); +const defaultLog = getLogger('paths/administrative-activities'); -export const GET: Operation = [logRequest('paths/administrative-activity', 'GET'), getAdministrativeActivities()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + getAdministrativeActivities() +]; + +export enum ADMINISTRATIVE_ACTIVITY_TYPE { + SYSTEM_ACCESS = 'System Access' +} + +export const getAllAdministrativeActivityTypes = (): string[] => Object.values(ADMINISTRATIVE_ACTIVITY_TYPE); export enum ADMINISTRATIVE_ACTIVITY_STATUS_TYPE { PENDING = 'Pending', @@ -25,7 +42,7 @@ GET.apiDoc = { tags: ['admin'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -33,8 +50,11 @@ GET.apiDoc = { in: 'query', name: 'type', schema: { - type: 'string', - enum: ['System Access'] + type: 'array', + items: { + type: 'string', + enum: getAllAdministrativeActivityTypes() + } } }, { @@ -58,32 +78,27 @@ GET.apiDoc = { type: 'array', items: { type: 'object', + required: ['id', 'type', 'type_name', 'status', 'status_name', 'create_date'], + additionalProperties: true, properties: { id: { - type: 'number', - description: 'Administrative activity row ID' + type: 'number' }, type: { - type: 'number', - description: 'Administrative activity type ID' + type: 'number' }, type_name: { - type: 'string', - description: 'Administrative activity type name' + type: 'string' }, status: { - type: 'number', - description: 'Administrative activity status type ID' + type: 'number' }, status_name: { - type: 'string', - description: 'Administrative activity status type name' - }, - description: { type: 'string' }, - notes: { - type: 'string' + description: { + type: 'string', + nullable: true }, data: { type: 'object', @@ -92,8 +107,13 @@ GET.apiDoc = { // Don't specify as this is a JSON blob column } }, + notes: { + type: 'string', + nullable: true + }, create_date: { - type: 'string' + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the project start date' } } } @@ -129,20 +149,16 @@ export function getAdministrativeActivities(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const administrativeActivityTypeName = (req.query?.type as string) || undefined; + const administrativeActivityTypes = (req.query.type as string[]) || getAllAdministrativeActivityTypes(); const administrativeActivityStatusTypes: string[] = - (req.query?.status as string[]) || getAllAdministrativeActivityStatusTypes(); + (req.query.status as string[]) || getAllAdministrativeActivityStatusTypes(); - const sqlStatement = getAdministrativeActivitiesSQL( - administrativeActivityTypeName, + const sqlStatement = queries.administrativeActivity.getAdministrativeActivitiesSQL( + administrativeActivityTypes, administrativeActivityStatusTypes ); - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); const response = await connection.query(sqlStatement.text, sqlStatement.values); diff --git a/api/src/paths/administrative-activity.test.ts b/api/src/paths/administrative-activity.test.ts index 40a38f2a20..21b2a6872a 100644 --- a/api/src/paths/administrative-activity.test.ts +++ b/api/src/paths/administrative-activity.test.ts @@ -2,12 +2,14 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as administrative_activity from './administrative-activity'; -import * as administrative_queries from '../queries/administrative-activity/administrative-activity-queries'; -import * as db from '../database/db'; -import { getMockDBConnection } from '../__mocks__/db'; import SQL from 'sql-template-strings'; +import * as db from '../database/db'; +import { HTTPError } from '../errors/custom-error'; +import administrative_queries from '../queries/administrative-activity'; import * as keycloak_utils from '../utils/keycloak-utils'; +import { getMockDBConnection } from '../__mocks__/db'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from './administrative-activities'; +import * as administrative_activity from './administrative-activity'; chai.use(sinonChai); @@ -38,7 +40,7 @@ describe('updateAccessRequest', () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { - return null; + return (null as unknown) as number; } }); @@ -48,8 +50,8 @@ describe('updateAccessRequest', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to identify system user ID'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to identify system user ID'); } }); @@ -68,8 +70,8 @@ describe('updateAccessRequest', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); @@ -95,8 +97,8 @@ describe('updateAccessRequest', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to submit administrative activity'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to submit administrative activity'); } }); @@ -127,8 +129,8 @@ describe('updateAccessRequest', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to submit administrative activity'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to submit administrative activity'); } }); @@ -190,14 +192,16 @@ describe('getPendingAccessRequestsCount', () => { it('should throw a 400 error when no user identifier', async () => { sinon.stub(keycloak_utils, 'getUserIdentifier').returns(null); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + try { const result = administrative_activity.getPendingAccessRequestsCount(); await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required userIdentifier'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required userIdentifier'); } }); @@ -217,8 +221,8 @@ describe('getPendingAccessRequestsCount', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -273,48 +277,6 @@ describe('getPendingAccessRequestsCount', () => { }); }); -describe('getUpdateAdministrativeActivityHandler', () => { - afterEach(() => { - sinon.restore(); - }); - - const sampleReq = { - keycloak_token: {}, - body: { - id: null, - status: null - } - } as any; - - it('should throw a 400 error when no administrativeActivityId', async () => { - try { - const result = administrative_activity.getUpdateAdministrativeActivityHandler(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body parameter: id'); - } - }); - - it('should throw a 400 error when no administrativeActivityStatusTypeId', async () => { - try { - const result = administrative_activity.getUpdateAdministrativeActivityHandler(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, id: 2 } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body parameter: status'); - } - }); -}); - describe('updateAdministrativeActivity', () => { afterEach(() => { sinon.restore(); @@ -322,24 +284,6 @@ describe('updateAdministrativeActivity', () => { const dbConnectionObj = getMockDBConnection(); - it('should throw a 400 error when failed to build putAdministrativeActivitySQL statement', async () => { - sinon.stub(administrative_queries, 'putAdministrativeActivitySQL').returns(null); - - try { - await administrative_activity.updateAdministrativeActivity(1, 2, { - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL put statement'); - } - }); - it('should throw a 500 error when failed to update administrative activity', async () => { sinon.stub(administrative_queries, 'putAdministrativeActivitySQL').returns(SQL`some`); @@ -350,7 +294,7 @@ describe('updateAdministrativeActivity', () => { }); try { - await administrative_activity.updateAdministrativeActivity(1, 2, { + await administrative_activity.updateAdministrativeActivity(1, ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.ACTIONED, { ...dbConnectionObj, systemUserId: () => { return 20; @@ -360,8 +304,8 @@ describe('updateAdministrativeActivity', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to update administrative activity'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to update administrative activity'); } }); }); diff --git a/api/src/paths/administrative-activity.ts b/api/src/paths/administrative-activity.ts index 96d34b1b43..e5ebeb4964 100644 --- a/api/src/paths/administrative-activity.ts +++ b/api/src/paths/administrative-activity.ts @@ -1,25 +1,27 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getAPIUserDBConnection, getDBConnection, IDBConnection } from '../database/db'; -import { HTTP400, HTTP500 } from '../errors/CustomError'; +import { ACCESS_REQUEST_ADMIN_EMAIL } from '../constants/notifications'; +import { getAPIUserDBConnection, IDBConnection } from '../database/db'; +import { HTTP400, HTTP500 } from '../errors/custom-error'; import { administrativeActivityResponseObject, hasPendingAdministrativeActivitiesResponseObject } from '../openapi/schemas/administrative-activity'; -import { - countPendingAdministrativeActivitiesSQL, - postAdministrativeActivitySQL, - putAdministrativeActivitySQL -} from '../queries/administrative-activity/administrative-activity-queries'; +import { queries } from '../queries/queries'; +import { GCNotifyService } from '../services/gcnotify-service'; import { getUserIdentifier } from '../utils/keycloak-utils'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from './administrative-activities'; const defaultLog = getLogger('paths/administrative-activity-request'); -export const POST: Operation = [logRequest('paths/administrative-activity', 'POST'), createAdministrativeActivity()]; -export const GET: Operation = [logRequest('paths/administrative-activity', 'GET'), getPendingAccessRequestsCount()]; +const ADMIN_EMAIL = process.env.GCNOTIFY_ADMIN_EMAIL || ''; +const APP_HOST = process.env.APP_HOST; +const NODE_ENV = process.env.NODE_ENV; + +export const POST: Operation = [createAdministrativeActivity()]; + +export const GET: Operation = [getPendingAccessRequestsCount()]; POST.apiDoc = { description: 'Create a new Administrative Activity.', @@ -137,7 +139,10 @@ export function createAdministrativeActivity(): RequestHandler { throw new HTTP500('Failed to identify system user ID'); } - const postAdministrativeActivitySQLStatement = postAdministrativeActivitySQL(systemUserId, req?.body); + const postAdministrativeActivitySQLStatement = queries.administrativeActivity.postAdministrativeActivitySQL( + systemUserId, + req?.body + ); if (!postAdministrativeActivitySQLStatement) { throw new HTTP500('Failed to build SQL insert statement'); @@ -160,6 +165,8 @@ export function createAdministrativeActivity(): RequestHandler { throw new HTTP500('Failed to submit administrative activity'); } + sendAccessRequestEmail(); + return res .status(200) .json({ id: administrativeActivityResult.id, date: administrativeActivityResult.create_date }); @@ -173,6 +180,17 @@ export function createAdministrativeActivity(): RequestHandler { }; } +function sendAccessRequestEmail() { + const gcnotifyService = new GCNotifyService(); + const url = `${APP_HOST}/admin/users?authLogin=true`; + const hrefUrl = `[click here.](${url})`; + gcnotifyService.sendEmailGCNotification(ADMIN_EMAIL, { + ...ACCESS_REQUEST_ADMIN_EMAIL, + subject: `${NODE_ENV}: ${ACCESS_REQUEST_ADMIN_EMAIL.subject}`, + body1: `${ACCESS_REQUEST_ADMIN_EMAIL.body1} ${hrefUrl}`, + footer: `${APP_HOST}` + }); +} /** * Get all projects. * @@ -189,7 +207,7 @@ export function getPendingAccessRequestsCount(): RequestHandler { throw new HTTP400('Missing required userIdentifier'); } - const sqlStatement = countPendingAdministrativeActivitiesSQL(userIdentifier); + const sqlStatement = queries.administrativeActivity.countPendingAdministrativeActivitiesSQL(userIdentifier); if (!sqlStatement) { throw new HTTP400('Failed to build SQL get statement'); @@ -214,119 +232,22 @@ export function getPendingAccessRequestsCount(): RequestHandler { }; } -export const PUT: Operation = [ - logRequest('paths/administrative-activity', 'PUT'), - getUpdateAdministrativeActivityHandler() -]; - -PUT.apiDoc = { - description: 'Update an existing administrative activity.', - tags: ['admin'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - requestBody: { - description: 'Administrative activity request object.', - content: { - 'application/json': { - schema: { - title: 'Administrative activity put object', - type: 'object', - required: ['id', 'status'], - properties: { - id: { - title: 'administrative activity record ID', - type: 'number' - }, - status: { - title: 'administrative activity status type code ID', - type: 'number' - } - } - } - } - } - }, - responses: { - 200: { - description: 'Put administrative activity OK' - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -/** - * Get a request handler to update an existing administrative activity. - * - * @returns {RequestHandler} - */ -export function getUpdateAdministrativeActivityHandler(): RequestHandler { - return async (req, res) => { - defaultLog.debug({ - label: 'getUpdateAdministrativeActivityHandler', - message: 'params', - req_body: req.body - }); - - const administrativeActivityId = Number(req.body?.id); - const administrativeActivityStatusTypeId = Number(req.body?.status); - - if (!administrativeActivityId) { - throw new HTTP400('Missing required body parameter: id'); - } - - if (!administrativeActivityStatusTypeId) { - throw new HTTP400('Missing required body parameter: status'); - } - - const connection = getDBConnection(req['keycloak_token']); - - try { - await connection.open(); - - await updateAdministrativeActivity(administrativeActivityId, administrativeActivityStatusTypeId, connection); - - await connection.commit(); - - return res.status(200).send(); - } catch (error) { - defaultLog.error({ label: 'getUpdateAdministrativeActivityHandler', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} - /** * Update an existing administrative activity. * * @param {number} administrativeActivityId - * @param {number} administrativeActivityStatusTypeId + * @param {ADMINISTRATIVE_ACTIVITY_STATUS_TYPE} administrativeActivityStatusTypeName * @param {IDBConnection} connection */ export const updateAdministrativeActivity = async ( administrativeActivityId: number, - administrativeActivityStatusTypeId: number, + administrativeActivityStatusTypeName: ADMINISTRATIVE_ACTIVITY_STATUS_TYPE, connection: IDBConnection ) => { - const sqlStatement = putAdministrativeActivitySQL(administrativeActivityId, administrativeActivityStatusTypeId); + const sqlStatement = queries.administrativeActivity.putAdministrativeActivitySQL( + administrativeActivityId, + administrativeActivityStatusTypeName + ); if (!sqlStatement) { throw new HTTP400('Failed to build SQL put statement'); diff --git a/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.test.ts b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.test.ts new file mode 100644 index 0000000000..853111a745 --- /dev/null +++ b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.test.ts @@ -0,0 +1,116 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { UserObject } from '../../../../models/user'; +import { UserService } from '../../../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../../administrative-activities'; +import * as administrative_activity from '../../../administrative-activity'; +import * as approve_request from './approve'; + +chai.use(sinonChai); + +describe('approveAccessRequest', () => { + afterEach(() => { + sinon.restore(); + }); + + it('throws an error if the identity source is not supported', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + userIdentifier: 1, + identitySource: 'fake-source', + roleIds: [1, 3] + }; + + const requestHandler = approve_request.approveAccessRequest(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (error) { + expect((error as HTTPError).status).to.equal(400); + expect((error as HTTPError).message).to.equal('Invalid user identity source'); + } + }); + + it('re-throws any error that is thrown', async () => { + const expectedError = new Error('test error'); + + const mockDBConnection = getMockDBConnection({ + open: () => { + throw expectedError; + } + }); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + userIdentifier: 1, + identitySource: 'idir', + roleIds: [1, 3] + }; + + const requestHandler = approve_request.approveAccessRequest(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (error) { + expect(error).to.equal(expectedError); + } + }); + + it('adds new system roles and updates administrative activity as actioned', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + administrativeActivityId: '1' + }; + + mockReq.body = { + userIdentifier: 'username', + identitySource: 'bceid', + roleIds: [1, 3] + }; + + const systemUserId = 4; + const existingRoleIds = [1, 2]; + const mockSystemUser: UserObject = { + id: systemUserId, + user_identifier: '', + record_end_date: '', + role_ids: existingRoleIds, + role_names: [] + }; + const ensureSystemUserStub = sinon.stub(UserService.prototype, 'ensureSystemUser').resolves(mockSystemUser); + + const addSystemRolesStub = sinon.stub(UserService.prototype, 'addUserSystemRoles'); + + const updateAdministrativeActivityStub = sinon.stub(administrative_activity, 'updateAdministrativeActivity'); + + const requestHandler = approve_request.approveAccessRequest(); + + await requestHandler(mockReq, mockRes, mockNext); + + const expectedRoleIdsToAdd = [3]; + + expect(ensureSystemUserStub).to.have.been.calledOnce; + expect(addSystemRolesStub).to.have.been.calledWith(systemUserId, expectedRoleIdsToAdd); + expect(updateAdministrativeActivityStub).to.have.been.calledWith( + 1, + ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.ACTIONED, + mockDBConnection + ); + }); +}); diff --git a/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.ts b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.ts new file mode 100644 index 0000000000..ccb8282962 --- /dev/null +++ b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/approve.ts @@ -0,0 +1,167 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_IDENTITY_SOURCE } from '../../../../constants/database'; +import { EXTERNAL_BCEID_IDENTITY_SOURCES, EXTERNAL_IDIR_IDENTITY_SOURCES } from '../../../../constants/keycloak'; +import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { UserService } from '../../../../services/user-service'; +import { convertUserIdentitySource } from '../../../../utils/keycloak-utils'; +import { getLogger } from '../../../../utils/logger'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../../administrative-activities'; +import { updateAdministrativeActivity } from '../../../administrative-activity'; + +const defaultLog = getLogger('paths/administrative-activity/system-access/{administrativeActivityId}/approve'); + +export const PUT: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + approveAccessRequest() +]; + +const UniqueUserIdentitySources = Array.from( + new Set([ + SYSTEM_IDENTITY_SOURCE.IDIR, + SYSTEM_IDENTITY_SOURCE.BCEID, + ...EXTERNAL_IDIR_IDENTITY_SOURCES, + ...EXTERNAL_BCEID_IDENTITY_SOURCES + ]) +); + +// Contains both uppercase and lowercase versions of the identity sources +const AllUserIdentitySources = [ + ...UniqueUserIdentitySources, + ...UniqueUserIdentitySources.map((item) => item.toLowerCase()) +]; + +PUT.apiDoc = { + description: "Update a user's system access request and add any specified system roles to the user.", + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'administrativeActivityId', + schema: { + type: 'number', + minimum: 1 + }, + required: true + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + required: ['userIdentifier', 'identitySource'], + properties: { + userIdentifier: { + type: 'string', + description: 'The user identifier for the user.' + }, + identitySource: { + type: 'string', + enum: AllUserIdentitySources + }, + roleIds: { + type: 'array', + items: { + type: 'number' + }, + description: + 'An array of role ids to add, if the access-request was approved. Ignored if the access-request was denied.' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Add system user roles to user OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function approveAccessRequest(): RequestHandler { + return async (req, res) => { + const administrativeActivityId = Number(req.params.administrativeActivityId); + + const userIdentifier = req.body.userIdentifier; + + // Convert identity sources that have multiple variations (ie: BCEID) into a single value supported by this app + const identitySource = convertUserIdentitySource(req.body.identitySource); + + if (!identitySource) { + throw new HTTP400('Invalid user identity source', [ + `Identity source <${req.body.identitySource}> is not a supported value.` + ]); + } + + const roleIds: number[] = req.body.roleIds || []; + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const userService = new UserService(connection); + + // Get the system user (adding or activating them if they already existed). + const systemUserObject = await userService.ensureSystemUser(userIdentifier, identitySource); + + // Filter out any system roles that have already been added to the user + const rolesIdsToAdd = roleIds.filter((roleId) => !systemUserObject.role_ids.includes(roleId)); + + if (rolesIdsToAdd?.length) { + // Add any missing roles (if any) + await userService.addUserSystemRoles(systemUserObject.id, rolesIdsToAdd); + } + + // Update the access request record status + await updateAdministrativeActivity( + administrativeActivityId, + ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.ACTIONED, + connection + ); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateAccessRequest', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.test.ts b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.test.ts new file mode 100644 index 0000000000..eebb0124ee --- /dev/null +++ b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.test.ts @@ -0,0 +1,66 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../../administrative-activities'; +import * as administrative_activity from '../../../administrative-activity'; +import * as reject_request from './reject'; + +chai.use(sinonChai); + +describe('rejectAccessRequest', () => { + afterEach(() => { + sinon.restore(); + }); + + it('re-throws any error that is thrown', async () => { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + administrativeActivityId: '1' + }; + + const expectedError = new Error('test error'); + + sinon.stub(administrative_activity, 'updateAdministrativeActivity').rejects(expectedError); + + const requestHandler = reject_request.rejectAccessRequest(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (error) { + expect(error).to.equal(expectedError); + } + }); + + it('updates administrative activity as rejected', async () => { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + administrativeActivityId: '1' + }; + + const updateAdministrativeActivityStub = sinon + .stub(administrative_activity, 'updateAdministrativeActivity') + .resolves(); + + const requestHandler = reject_request.rejectAccessRequest(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(updateAdministrativeActivityStub).to.have.been.calledOnceWith( + 1, + ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.REJECTED, + mockDBConnection + ); + }); +}); diff --git a/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.ts b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.ts new file mode 100644 index 0000000000..7bedba7d03 --- /dev/null +++ b/api/src/paths/administrative-activity/system-access/{administrativeActivityId}/reject.ts @@ -0,0 +1,92 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../utils/logger'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../../administrative-activities'; +import { updateAdministrativeActivity } from '../../../administrative-activity'; + +const defaultLog = getLogger('paths/administrative-activity/system-access/{administrativeActivityId}/reject'); + +export const PUT: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + rejectAccessRequest() +]; + +PUT.apiDoc = { + description: "Update a user's system access request and add any specified system roles to the user.", + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'administrativeActivityId', + schema: { + type: 'number', + minimum: 1 + }, + required: true + } + ], + responses: { + 200: { + description: 'Add system user roles to user OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function rejectAccessRequest(): RequestHandler { + return async (req, res) => { + const administrativeActivityId = Number(req.params.administrativeActivityId); + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + await updateAdministrativeActivity( + administrativeActivityId, + ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.REJECTED, + connection + ); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateAccessRequest', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/codes.test.ts b/api/src/paths/codes.test.ts index ff4d8175c0..f26aec385a 100644 --- a/api/src/paths/codes.test.ts +++ b/api/src/paths/codes.test.ts @@ -2,10 +2,11 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as codes from './codes'; import * as db from '../database/db'; -import * as code_utils from '../utils/code-utils'; +import { HTTPError } from '../errors/custom-error'; +import { CodeService } from '../services/code-service'; import { getMockDBConnection } from '../__mocks__/db'; +import * as codes from './codes'; chai.use(sinonChai); @@ -37,7 +38,7 @@ describe('codes', () => { it('should throw a 500 error when fails to fetch codes', async () => { sinon.stub(db, 'getAPIUserDBConnection').returns(dbConnectionObj); - sinon.stub(code_utils, 'getAllCodeSets').resolves(null); + sinon.stub(CodeService.prototype, 'getAllCodeSets').resolves(undefined); try { const result = codes.getAllCodes(); @@ -45,14 +46,14 @@ describe('codes', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to fetch codes'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to fetch codes'); } }); it('should return the fetched codes on success', async () => { sinon.stub(db, 'getAPIUserDBConnection').returns(dbConnectionObj); - sinon.stub(code_utils, 'getAllCodeSets').resolves({ + sinon.stub(CodeService.prototype, 'getAllCodeSets').resolves({ management_action_type: { id: 1, name: 'management action type' } } as any); @@ -67,7 +68,7 @@ describe('codes', () => { const expectedError = new Error('cannot process request'); sinon.stub(db, 'getAPIUserDBConnection').returns(dbConnectionObj); - sinon.stub(code_utils, 'getAllCodeSets').rejects(expectedError); + sinon.stub(CodeService.prototype, 'getAllCodeSets').rejects(expectedError); try { const result = codes.getAllCodes(); @@ -75,7 +76,7 @@ describe('codes', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); + expect((actualError as HTTPError).message).to.equal(expectedError.message); } }); }); diff --git a/api/src/paths/codes.ts b/api/src/paths/codes.ts index 68e44ee82c..c6ceb9ac61 100644 --- a/api/src/paths/codes.ts +++ b/api/src/paths/codes.ts @@ -1,23 +1,17 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { getAPIUserDBConnection } from '../database/db'; -import { HTTP500 } from '../errors/CustomError'; -import { getAllCodeSets } from '../utils/code-utils'; +import { HTTP500 } from '../errors/custom-error'; +import { CodeService } from '../services/code-service'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; const defaultLog = getLogger('paths/code'); -export const GET: Operation = [logRequest('paths/code', 'GET'), getAllCodes()]; +export const GET: Operation = [getAllCodes()]; GET.apiDoc = { description: 'Get all Codes.', tags: ['code'], - security: [ - { - Bearer: [] - } - ], responses: { 200: { description: 'Code response object.', @@ -113,6 +107,20 @@ GET.apiDoc = { } } }, + coordinator_agency: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, region: { type: 'array', items: { @@ -151,6 +159,9 @@ GET.apiDoc = { }, name: { type: 'string' + }, + is_first_nation: { + type: 'boolean' } } } @@ -216,6 +227,104 @@ GET.apiDoc = { } } } + }, + project_role: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + regional_offices: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + administrative_activity_status_type: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + field_methods: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + ecological_seasons: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + intended_outcomes: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } + }, + vantage_codes: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + } + } + } } } } @@ -250,7 +359,13 @@ export function getAllCodes(): RequestHandler { const connection = getAPIUserDBConnection(); try { - const allCodeSets = await getAllCodeSets(connection); + await connection.open(); + + const codeService = new CodeService(connection); + + const allCodeSets = await codeService.getAllCodeSets(); + + await connection.commit(); if (!allCodeSets) { throw new HTTP500('Failed to fetch codes'); @@ -259,6 +374,7 @@ export function getAllCodes(): RequestHandler { return res.status(200).json(allCodeSets); } catch (error) { defaultLog.error({ label: 'getAllCodes', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/draft.test.ts b/api/src/paths/draft.test.ts index 4211854351..dedb8451c4 100644 --- a/api/src/paths/draft.test.ts +++ b/api/src/paths/draft.test.ts @@ -5,7 +5,8 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../database/db'; -import * as draft_queries from '../queries/draft-queries'; +import { HTTPError } from '../errors/custom-error'; +import draft_queries from '../queries/project/draft'; import { getMockDBConnection } from '../__mocks__/db'; import * as draft from './draft'; @@ -52,8 +53,8 @@ describe('draft', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to identify system user ID'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to identify system user ID'); } }); @@ -72,8 +73,8 @@ describe('draft', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); @@ -95,8 +96,8 @@ describe('draft', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required param name'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param name'); } }); @@ -118,8 +119,8 @@ describe('draft', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required param data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param data'); } }); @@ -149,8 +150,8 @@ describe('draft', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to save draft'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to save draft'); } }); @@ -175,8 +176,8 @@ describe('draft', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to save draft'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to save draft'); } }); @@ -255,7 +256,7 @@ describe('draft', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); + expect((actualError as HTTPError).message).to.equal(expectedError.message); } }); }); @@ -283,8 +284,8 @@ describe('draft', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required param id'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param id'); } }); @@ -306,8 +307,8 @@ describe('draft', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required param name'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param name'); } }); @@ -329,8 +330,8 @@ describe('draft', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required param data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param data'); } }); @@ -349,8 +350,8 @@ describe('draft', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); @@ -380,8 +381,8 @@ describe('draft', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to update draft'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update draft'); } }); @@ -406,8 +407,8 @@ describe('draft', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to update draft'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update draft'); } }); diff --git a/api/src/paths/draft.ts b/api/src/paths/draft.ts index 07828b684e..a0056fd432 100644 --- a/api/src/paths/draft.ts +++ b/api/src/paths/draft.ts @@ -1,17 +1,45 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; +import { PROJECT_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; +import { HTTP400 } from '../errors/custom-error'; import { draftResponseObject } from '../openapi/schemas/draft'; -import { postDraftSQL, putDraftSQL } from '../queries/draft-queries'; +import { queries } from '../queries/queries'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; const defaultLog = getLogger('paths/draft'); -export const PUT: Operation = [logRequest('paths/draft', 'PUT'), updateDraft()]; -export const POST: Operation = [logRequest('paths/draft', 'POST'), createDraft()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateDraft() +]; + +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + + createDraft() +]; const postPutResponses = { 200: { @@ -46,7 +74,7 @@ POST.apiDoc = { tags: ['draft'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -82,7 +110,7 @@ PUT.apiDoc = { tags: ['draft'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -143,7 +171,7 @@ export function createDraft(): RequestHandler { throw new HTTP400('Missing required param data'); } - const postDraftSQLStatement = postDraftSQL(systemUserId, req.body.name, req.body.data); + const postDraftSQLStatement = queries.project.draft.postDraftSQL(systemUserId, req.body.name, req.body.data); if (!postDraftSQLStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -192,7 +220,7 @@ export function updateDraft(): RequestHandler { throw new HTTP400('Missing required param data'); } - const putDraftSQLStatement = putDraftSQL(req.body.id, req.body.name, req.body.data); + const putDraftSQLStatement = queries.project.draft.putDraftSQL(req.body.id, req.body.name, req.body.data); if (!putDraftSQLStatement) { throw new HTTP400('Failed to build SQL update statement'); diff --git a/api/src/paths/draft/{draftId}/delete.test.ts b/api/src/paths/draft/{draftId}/delete.test.ts index 80349935f1..4029c607ba 100644 --- a/api/src/paths/draft/{draftId}/delete.test.ts +++ b/api/src/paths/draft/{draftId}/delete.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as deleteDraftProject from './delete'; -import * as db from '../../../database/db'; -import * as deleteDraft_queries from '../../../queries/draft-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import draft_queries from '../../../queries/project/draft'; import { getMockDBConnection } from '../../../__mocks__/db'; +import * as deleteDraftProject from './delete'; chai.use(sinonChai); @@ -49,8 +50,8 @@ describe('delete a draft project', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `draftId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `draftId`'); } }); @@ -62,7 +63,7 @@ describe('delete a draft project', () => { } }); - sinon.stub(deleteDraft_queries, 'deleteDraftSQL').returns(null); + sinon.stub(draft_queries, 'deleteDraftSQL').returns(null); try { const result = deleteDraftProject.deleteDraft(); @@ -70,8 +71,8 @@ describe('delete a draft project', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -88,7 +89,7 @@ describe('delete a draft project', () => { query: mockQuery }); - sinon.stub(deleteDraft_queries, 'deleteDraftSQL').returns(SQL`something`); + sinon.stub(draft_queries, 'deleteDraftSQL').returns(SQL`something`); const result = deleteDraftProject.deleteDraft(); diff --git a/api/src/paths/draft/{draftId}/delete.ts b/api/src/paths/draft/{draftId}/delete.ts index 2377c180f1..1638bb1f2a 100644 --- a/api/src/paths/draft/{draftId}/delete.ts +++ b/api/src/paths/draft/{draftId}/delete.ts @@ -1,23 +1,34 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400 } from '../../../errors/CustomError'; +import { HTTP400 } from '../../../errors/custom-error'; +import { queries } from '../../../queries/queries'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; import { getLogger } from '../../../utils/logger'; -import { SYSTEM_ROLE } from '../../../constants/roles'; -import { deleteDraftSQL } from '../../../queries/draft-queries'; const defaultLog = getLogger('/api/draft/{draftId}/delete'); -export const DELETE: Operation = [deleteDraft()]; +export const DELETE: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + deleteDraft() +]; DELETE.apiDoc = { description: 'Delete a draft record.', tags: ['attachment'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -41,9 +52,18 @@ DELETE.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -63,7 +83,7 @@ export function deleteDraft(): RequestHandler { try { await connection.open(); - const deleteDraftSQLStatement = deleteDraftSQL(Number(req.params.draftId)); + const deleteDraftSQLStatement = queries.project.draft.deleteDraftSQL(Number(req.params.draftId)); if (!deleteDraftSQLStatement) { throw new HTTP400('Failed to build SQL delete statement'); diff --git a/api/src/paths/draft/{draftId}/get.test.ts b/api/src/paths/draft/{draftId}/get.test.ts index 1cd5674d05..af6b6dec5d 100644 --- a/api/src/paths/draft/{draftId}/get.test.ts +++ b/api/src/paths/draft/{draftId}/get.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as viewDraftProject from './get'; -import * as db from '../../../database/db'; -import * as draft_queries from '../../../queries/draft-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import draft_queries from '../../../queries/project/draft'; import { getMockDBConnection } from '../../../__mocks__/db'; +import * as viewDraftProject from './get'; chai.use(sinonChai); @@ -53,8 +54,8 @@ describe('gets a draft project', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); diff --git a/api/src/paths/draft/{draftId}/get.ts b/api/src/paths/draft/{draftId}/get.ts index 40dc25416f..ab7a09c07d 100644 --- a/api/src/paths/draft/{draftId}/get.ts +++ b/api/src/paths/draft/{draftId}/get.ts @@ -2,22 +2,34 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400 } from '../../../errors/CustomError'; +import { HTTP400 } from '../../../errors/custom-error'; import { draftGetResponseObject } from '../../../openapi/schemas/draft'; -import { getDraftSQL } from '../../../queries/draft-queries'; +import { queries } from '../../../queries/queries'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; const defaultLog = getLogger('paths/draft/{draftId}'); -export const GET: Operation = [logRequest('paths/draft/{draftId}', 'GET'), getSingleDraft()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + getSingleDraft() +]; GET.apiDoc = { description: 'Get a draft.', tags: ['draft'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -48,7 +60,7 @@ GET.apiDoc = { $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -69,7 +81,7 @@ export function getSingleDraft(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getDraftSQLStatement = getDraftSQL(Number(req.params.draftId)); + const getDraftSQLStatement = queries.project.draft.getDraftSQL(Number(req.params.draftId)); if (!getDraftSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/drafts.test.ts b/api/src/paths/drafts.test.ts index 69b4247343..63764eaf88 100644 --- a/api/src/paths/drafts.test.ts +++ b/api/src/paths/drafts.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as drafts from './drafts'; -import * as db from '../database/db'; -import * as draft_queries from '../queries/draft-queries'; import SQL from 'sql-template-strings'; +import * as db from '../database/db'; +import { HTTPError } from '../errors/custom-error'; +import draft_queries from '../queries/project/draft'; import { getMockDBConnection } from '../__mocks__/db'; +import * as drafts from './drafts'; chai.use(sinonChai); @@ -43,8 +44,8 @@ describe('drafts', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to identify system user ID'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to identify system user ID'); } }); @@ -63,8 +64,8 @@ describe('drafts', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -89,8 +90,8 @@ describe('drafts', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get drafts'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get drafts'); } }); diff --git a/api/src/paths/drafts.ts b/api/src/paths/drafts.ts index 0b0de73ec6..c181fbe597 100644 --- a/api/src/paths/drafts.ts +++ b/api/src/paths/drafts.ts @@ -1,23 +1,33 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; +import { HTTP400 } from '../errors/custom-error'; import { draftResponseObject } from '../openapi/schemas/draft'; -import { getDraftsSQL } from '../queries/draft-queries'; +import { queries } from '../queries/queries'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; const defaultLog = getLogger('paths/drafts'); -export const GET: Operation = [logRequest('paths/drafts', 'GET'), getDraftList()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getDraftList() +]; GET.apiDoc = { description: 'Get all Drafts.', tags: ['draft'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -70,7 +80,7 @@ export function getDraftList(): RequestHandler { throw new HTTP400('Failed to identify system user ID'); } - const getDraftsSQLStatement = getDraftsSQL(systemUserId); + const getDraftsSQLStatement = queries.project.draft.getDraftsSQL(systemUserId); if (!getDraftsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/dwc/eml.test.ts b/api/src/paths/dwc/eml.test.ts new file mode 100644 index 0000000000..2fb3611698 --- /dev/null +++ b/api/src/paths/dwc/eml.test.ts @@ -0,0 +1,658 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import eml_queries from '../../queries/dwc'; +import { getMockDBConnection } from '../../__mocks__/db'; +import * as eml from './eml'; + +chai.use(sinonChai); + +const dbConnectionObj = getMockDBConnection({ + systemUserId: () => { + return 20; + } +}); + +describe('getSurveyDataPackageEML', () => { + const sampleReq = { + keycloak_token: {}, + body: { + data_package_id: 1 + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no data package id is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = eml.getSurveyDataPackageEML(); + await requestHandler( + { ...sampleReq, body: { ...sampleReq.body, data_package_id: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Missing required body param `data_package_id`.'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getSurveyOccurrenceSubmission', () => { + const sampleReq = { + keycloak_token: {}, + body: { + data_package_id: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no data package id is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + await eml.getSurveyOccurrenceSubmission(sampleReq.data_package_id, dbConnectionObj); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal( + 'Failed to acquire distinct survey occurrence submission record' + ); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no sql statement returned for getSurveyOccurrenceSubmissionSQL', async () => { + const fake = sinon.replace(eml_queries, 'getSurveyOccurrenceSubmissionSQL', sinon.fake.returns(null)); + + try { + await eml.getSurveyOccurrenceSubmission(sampleReq.data_package_id, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no rows returned', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(eml_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + + try { + await eml.getSurveyOccurrenceSubmission(sampleReq.data_package_id, dbConnectionObj); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal( + 'Failed to acquire distinct survey occurrence submission record' + ); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getDataPackage', () => { + const sampleReq = { + keycloak_token: {}, + body: { + data_package_id: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no data package id is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + await eml.getDataPackage(sampleReq.data_package_id, dbConnectionObj); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to acquire data package record'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no sql statement returned for getDataPackageSQL', async () => { + const fake = sinon.replace(eml_queries, 'getDataPackageSQL', sinon.fake.returns(null)); + + try { + await eml.getDataPackage(sampleReq.data_package_id, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no rows returned', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(eml_queries, 'getDataPackageSQL').returns(SQL`something`); + + try { + await eml.getDataPackage(sampleReq.data_package_id, dbConnectionObj); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to acquire data package record'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getPublishedSurveyStatus', () => { + const sampleReq = { + keycloak_token: {}, + body: { + occurrenceSubmissionId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getDataPackageSQL', async () => { + const fake = sinon.replace(eml_queries, 'getPublishedSurveyStatusSQL', sinon.fake.returns(null)); + + try { + await eml.getPublishedSurveyStatus(sampleReq.data_package_id, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getSurvey', () => { + const sampleReq = { + keycloak_token: {}, + body: { + surveyId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getSurveySQL', async () => { + const fake = sinon.replace(eml_queries, 'getSurveySQL', sinon.fake.returns(null)); + + try { + await eml.getSurvey(sampleReq.surveyId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProject', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectSQL', sinon.fake.returns(null)); + + try { + await eml.getProject(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getSurveyFundingSource', () => { + const sampleReq = { + keycloak_token: {}, + body: { + surveyId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getSurveyFundingSourceSQL', async () => { + const fake = sinon.replace(eml_queries, 'getSurveyFundingSourceSQL', sinon.fake.returns(null)); + + try { + await eml.getSurveyFundingSource(sampleReq.surveyId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectFundingSource', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getSurveyFundingSourceSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectFundingSourceSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectFundingSource(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getSurveyBoundingBox', () => { + const sampleReq = { + keycloak_token: {}, + body: { + surveyId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getSurveyBoundingBoxSQL', async () => { + const fake = sinon.replace(eml_queries, 'getGeometryBoundingBoxSQL', sinon.fake.returns(null)); + + try { + await eml.getSurveyBoundingBox(sampleReq.surveyId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectBoundingBox', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectBoundingBoxSQL', async () => { + const fake = sinon.replace(eml_queries, 'getGeometryBoundingBoxSQL', sinon.fake.returns(null)); + + try { + await eml.getSurveyBoundingBox(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectBoundingBox', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectBoundingBoxSQL', async () => { + const fake = sinon.replace(eml_queries, 'getGeometryBoundingBoxSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectBoundingBox(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getSurveyPolygons', () => { + const sampleReq = { + keycloak_token: {}, + body: { + surveyId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getSurveyPolygonsSQL', async () => { + const fake = sinon.replace(eml_queries, 'getGeometryPolygonsSQL', sinon.fake.returns(null)); + + try { + await eml.getSurveyPolygons(sampleReq.surveyId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectPolygons', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectPolygonsSQL', async () => { + const fake = sinon.replace(eml_queries, 'getGeometryPolygonsSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectPolygons(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getFocalTaxonomicCoverage', () => { + const sampleReq = { + keycloak_token: {}, + body: { + surveyId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getTaxonomicCoverageSQL', async () => { + const fake = sinon.replace(eml_queries, 'getTaxonomicCoverageSQL', sinon.fake.returns(null)); + + try { + await eml.getFocalTaxonomicCoverage(sampleReq.surveyId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectIucnConservation', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectIucnConservationSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectIucnConservationSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectIucnConservation(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectStakeholderPartnership', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectStakeholderPartnershipSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectStakeholderPartnershipSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectStakeholderPartnership(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectActivity', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectActivitySQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectActivitySQL', sinon.fake.returns(null)); + + try { + await eml.getProjectActivity(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectClimateInitiative', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectClimateInitiativeSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectClimateInitiativeSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectClimateInitiative(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectFirstNations', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectFirstNationsSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectFirstNationsSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectFirstNations(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); + +describe('getProjectManagementActions', () => { + const sampleReq = { + keycloak_token: {}, + body: { + projectId: null + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for getProjectManagementActionsSQL', async () => { + const fake = sinon.replace(eml_queries, 'getProjectManagementActionsSQL', sinon.fake.returns(null)); + + try { + await eml.getProjectManagementActions(sampleReq.projectId, dbConnectionObj); + + fake.should.have.returned(null); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); diff --git a/api/src/paths/dwc/eml.ts b/api/src/paths/dwc/eml.ts new file mode 100644 index 0000000000..9e75847f6e --- /dev/null +++ b/api/src/paths/dwc/eml.ts @@ -0,0 +1,887 @@ +import AdmZip from 'adm-zip'; +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import * as xml2js from 'xml2js'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../database/db'; +import { HTTP400, HTTP500 } from '../../errors/custom-error'; +import { queries } from '../../queries/queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { getDbCharacterSystemMetaDataConstant } from '../../utils/db-constant-utils'; +import { getFileFromS3, uploadBufferToS3 } from '../../utils/file-utils'; +import { getLogger } from '../../utils/logger'; +import { MediaFile } from '../../utils/media/media-file'; +import { parseS3File, parseUnknownZipFile } from '../../utils/media/media-utils'; + +const simsEmlVersion = '1.0.0'; + +type Eml = { [k: string]: any }; + +const defaultLog = getLogger('paths/dwc/eml'); + +const defaultEMLFileName = 'eml.xml'; +const defaultEMLMimeType = 'application/xml'; +const defaultArchiveMimeType = 'application/zip'; + +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyDataPackageEML(), + sendResponse() +]; + +POST.apiDoc = { + description: 'Produces an Ecological Metadata Language (EML) extract for a target data package.', + tags: ['eml', 'dwc'], + security: [ + { + Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR] + } + ], + requestBody: { + description: 'Request body', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['data_package_id'], + properties: { + project_id: { + type: 'number' + }, + data_package_id: { + description: 'A data package ID', + type: 'number', + example: 1 + }, + supplied_title: { + description: 'A user supplied title', + type: 'string', + example: 'My dataset title' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Ecological Metadata Language (EML) extract production OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function getSurveyDataPackageEML(): RequestHandler { + return async (req, res, next) => { + defaultLog.debug({ label: 'getSurveyDataPackageEML', message: 'params', files: req.body }); + + const connection = getDBConnection(req['keycloak_token']); + + if (!req.body.data_package_id) { + throw new HTTP400('Missing required body param `data_package_id`.'); + } + + try { + await connection.open(); + + // get required data + const occurrenceSubmission = await getSurveyOccurrenceSubmission(req.body.data_package_id, connection); + + // get the EML data for the survey + const dataPackageEML = await getDataPackageEML(req.body.data_package_id, connection, req.body.supplied_title); + + await connection.commit(); + + // get the archive file from s3 + if (!occurrenceSubmission.output_key) { + throw new HTTP400('No S3 target output key found'); + } + const s3Key = occurrenceSubmission.output_key; + const s3File = await getFileFromS3(s3Key); + + if (!s3File) { + throw new HTTP500('Failed to get file from S3'); + } + + // parse the archive file and add EML file + const archiveFile = parseS3File(s3File); + const mediaFiles = parseUnknownZipFile(archiveFile.buffer); + mediaFiles.push(new MediaFile(defaultEMLFileName, defaultEMLMimeType, Buffer.from(dataPackageEML))); + + // build the archive zip file + const dwcArchiveZip = new AdmZip(); + mediaFiles.forEach((file) => dwcArchiveZip.addFile(file.fileName, file.buffer)); + + // upload archive to s3 + await uploadBufferToS3(dwcArchiveZip.toBuffer(), defaultArchiveMimeType, s3Key); + + next(); + } catch (error) { + defaultLog.error({ label: 'getSurveyDataPackageEML', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} + +export function sendResponse(): RequestHandler { + return async (req, res) => { + return res.status(200).send(); + }; +} + +/** + * Get database application constants value. + * + * @param {number} dataPackageId + * @param {IDBConnection} connection + * @param {string} suppliedTitle + * @return {*} {Promise} + */ +export const getDataPackageEML = async ( + dataPackageId: number, + connection: IDBConnection, + suppliedTitle?: string +): Promise => { + defaultLog.debug({ + label: 'getDataPackageEML', + message: 'params', + dataPackageId, + suppliedTitle + }); + + // get all required data + const dataPackage = await getDataPackage(dataPackageId, connection); + const occurrenceSubmission = await getSurveyOccurrenceSubmission(dataPackageId, connection); + const publishedSurveyStatus = await getPublishedSurveyStatus( + occurrenceSubmission.occurrence_submission_id, + connection + ); + const survey = await getSurvey(occurrenceSubmission.survey_id, connection); + const project = await getProject(survey.project_id, connection); + const surveyFundingSource = await getSurveyFundingSource(survey.survey_id, connection); + const projectFundingSource = await getProjectFundingSource(project.project_id, connection); + const surveyBoundingBox = await getSurveyBoundingBox(survey.survey_id, connection); + const surveyPolygons = await getSurveyPolygons(survey.survey_id, connection); + const projectBoundingBox = await getProjectBoundingBox(project.project_id, connection); + const projectPolygons = await getProjectPolygons(project.project_id, connection); + const focalTaxonomicCoverage = await getFocalTaxonomicCoverage(survey.survey_id, connection); + const projectIucnConservation = await getProjectIucnConservation(project.project_id, connection); + const projectStakeholderPartnership = await getProjectStakeholderPartnership(project.project_id, connection); + const projectActivity = await getProjectActivity(project.project_id, connection); + const projectClimateInitiative = await getProjectClimateInitiative(project.project_id, connection); + const projectFirstNations = await getProjectFirstNations(project.project_id, connection); + const projectManagementActions = await getProjectManagementActions(project.project_id, connection); + const surveyProprietor = await getSurveyProprietor(survey.survey_id, connection); + // database constants + const simsProviderURL = checkProvided(await getDbCharacterSystemMetaDataConstant('PROVIDER_URL', connection)); + const securityProviderURL = checkProvided( + await getDbCharacterSystemMetaDataConstant('SECURITY_PROVIDER_URL', connection) + ); + const organizationFullName = checkProvided( + await getDbCharacterSystemMetaDataConstant('ORGANIZATION_NAME_FULL', connection) + ); + const organizationURL = checkProvided(await getDbCharacterSystemMetaDataConstant('ORGANIZATION_URL', connection)); + const intellectualRights = checkProvided( + await getDbCharacterSystemMetaDataConstant('INTELLECTUAL_RIGHTS', connection) + ); + const taxonomicProviderURL = checkProvided( + await getDbCharacterSystemMetaDataConstant('TAXONOMIC_PROVIDER_URL', connection) + ); + + // build eml object + const eml: Eml = { + 'eml:eml': { + $: { + packageId: 'urn:uuid:' + dataPackage.uuid, + system: simsProviderURL, + 'xmlns:eml': 'https://eml.ecoinformatics.org/eml-2.2.0', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xmlns:stmml': 'http://www.xml-cml.org/schema/stmml-1.1', + 'xsi:schemaLocation': 'https://eml.ecoinformatics.org/eml-2.2.0 xsd/eml.xsd' + } + } + }; + + const emlRoot = eml['eml:eml']; + + emlRoot.access = { + $: { authSystem: securityProviderURL, order: 'allowFirst' }, + allow: { principal: 'public', permission: 'read' } + }; + + emlRoot.dataset = { $: { system: simsProviderURL, id: dataPackage.uuid } }; + + if (suppliedTitle) { + emlRoot.dataset.title = suppliedTitle; + } else if (dataPackage) { + emlRoot.dataset.title = dataPackage.uuid; + } + + emlRoot.dataset.creator = { organizationName: project.coordinator_agency_name }; + + emlRoot.dataset.metadataProvider = { organizationName: organizationFullName, onlineUrl: organizationURL }; + + // TODO determine if this can be called without a publish date and if so what value to use? + emlRoot.dataset.pubDate = publishedSurveyStatus.rows[0].status_event_timestamp.toISOString().split('T')[0]; + + emlRoot.dataset.language = 'english'; + + emlRoot.dataset.intellectualRights = { para: intellectualRights }; + + emlRoot.dataset.contact = {}; + if (project.coordinator_public) { + emlRoot.dataset.contact = { + individualName: { givenName: project.coordinator_first_name, surName: project.coordinator_last_name }, + electronicMailAddress: project.coordinator_email_address + }; + } else { + emlRoot.dataset.contact = { organizationName: organizationFullName, onlineUrl: organizationURL }; + } + + // both projects and surveys are represented as "projects" in EML + // main EML "project" is the survey + emlRoot.dataset.project = { $: { id: survey.uuid, system: simsProviderURL }, title: survey.name }; + if (project.coordinator_public) { + emlRoot.dataset.project.personnel = { + individualName: { givenName: survey.lead_first_name, surName: survey.lead_last_name }, + organizationName: project.coordinator_agency_name, + role: 'pointOfContact' + }; + } else { + emlRoot.dataset.project.personnel = { + organizationName: organizationFullName, + onlineUrl: organizationURL, + role: 'custodianSteward' + }; + } + + emlRoot.dataset.project.abstract = { section: { title: 'Objectives', para: survey.objectives } }; + + if (surveyFundingSource.length) { + emlRoot.dataset.project.funding = getFundingEML(surveyFundingSource); + } + + emlRoot.dataset.project.studyAreaDescription = { coverage: {} }; + const surveyGeographicDescription = survey.location_description + ? survey.location_name + ' - ' + survey.location_description + : survey.location_name; + emlRoot.dataset.project.studyAreaDescription.coverage.geographicCoverage = getGeographicCoverageEML( + surveyGeographicDescription, + surveyBoundingBox, + surveyPolygons + ); + + emlRoot.dataset.project.studyAreaDescription.coverage.temporalCoverage = getTemporalCoverageEML(survey); + + emlRoot.dataset.project.studyAreaDescription.coverage.taxonomicCoverage = { taxonomicClassification: [] }; + focalTaxonomicCoverage.rows.forEach(function (row: any, i: number) { + emlRoot.dataset.project.studyAreaDescription.coverage.taxonomicCoverage.taxonomicClassification[i] = { + taxonRankName: row.tty_name, + taxonRankValue: row.unit_name1 + ' ' + row.unit_name2, + commonName: row.english_name, + taxonId: { $: { provider: taxonomicProviderURL }, _: row.code } + }; + }); + + emlRoot.dataset.project.relatedProject = { $: { id: project.uuid, system: simsProviderURL }, title: project.name }; + if (project.coordinator_public) { + emlRoot.dataset.project.relatedProject.personnel = { + individualName: { givenName: project.coordinator_first_name, surName: project.coordinator_last_name }, + organizationName: project.coordinator_agency_name, + electronicMailAddress: project.coordinator_email_address, + role: 'pointOfContact' + }; + } else { + emlRoot.dataset.project.relatedProject.personnel = { + organizationName: organizationFullName, + onlineUrl: organizationURL, + role: 'custodianSteward' + }; + } + + emlRoot.dataset.project.relatedProject.abstract = { + section: [ + { title: 'Objectives', para: project.objectives }, + { title: 'Caveats', para: project.caveats }, + { title: 'Comments', para: project.comments } + ] + }; + + if (projectFundingSource.length) { + emlRoot.dataset.project.relatedProject.funding = getFundingEML(projectFundingSource); + } + + emlRoot.dataset.project.relatedProject.studyAreaDescription = { coverage: {} }; + emlRoot.dataset.project.relatedProject.studyAreaDescription.coverage.geographicCoverage = getGeographicCoverageEML( + checkProvided(project.location_description), + projectBoundingBox, + projectPolygons + ); + + emlRoot.additionalMetadata = []; + let additionalMetadataCount = 0; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: dataPackage.uuid, + metadata: { simsEML: { type: 'survey', version: simsEmlVersion } } + }; + + if (projectIucnConservation.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { IUCNConservationActions: { IUCNConservationAction: [] } } + }; + projectIucnConservation.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.IUCNConservationActions.IUCNConservationAction[i] = { + IUCNConservationActionLevel1Classification: row.level_1_name, + IUCNConservationActionLevel2SubClassification: row.level_2_name, + IUCNConservationActionLevel3SubClassification: row.level_3_name + }; + }); + } + + if (projectStakeholderPartnership.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { stakeholderPartnerships: { stakeholderPartnership: [] } } + }; + projectStakeholderPartnership.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.stakeholderPartnerships.stakeholderPartnership[i] = { + name: row.name + }; + }); + } + + if (projectActivity.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { projectActivities: { projectActivity: [] } } + }; + projectActivity.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.projectActivities.projectActivity[i] = { + name: row.name + }; + }); + } + + if (projectClimateInitiative.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { climateChangeInitiatives: { climateChangeInitiative: [] } } + }; + projectClimateInitiative.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.climateChangeInitiatives.climateChangeInitiative[ + i + ] = { + name: row.name + }; + }); + } + + if (projectFirstNations.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { firstNations: { firstNation: [] } } + }; + projectFirstNations.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.firstNations.firstNation[i] = { + name: row.name + }; + }); + } + + if (projectManagementActions.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { managementActionTypes: { managementActionType: [] } } + }; + projectManagementActions.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.managementActionTypes.managementActionType[i] = { + name: row.name + }; + }); + } + + if (surveyProprietor.rowCount) { + additionalMetadataCount++; + emlRoot.additionalMetadata[additionalMetadataCount] = { + describes: project.uuid, + metadata: { surveyProprietors: { surveyProprietor: [] } } + }; + surveyProprietor.rows.forEach(function (row: any, i: number) { + emlRoot.additionalMetadata[additionalMetadataCount].metadata.surveyProprietors.surveyProprietor[i] = { + firstNationsName: row.first_nations_name, + proprietorType: row.proprietor_type_name, + rationale: row.rationale, + proprietorName: row.proprietor_name, + DISARequired: row.disa_required ? 'YES' : 'NO' + }; + }); + } + + // convert object to xml + const builder = new xml2js.Builder(); + return builder.buildObject(eml); +}; + +/** + * Return temporal coverage eml. + * + * @param {*} projectRow + * @return {Eml} + */ +const getTemporalCoverageEML = (projectRow: any): Eml => { + const temporalCoverage: Eml = { + rangeOfDates: { beginDate: { calendarDate: projectRow.start_date }, endDate: { calendarDate: projectRow.end_date } } + }; + + return temporalCoverage; +}; + +/** + * Return geographic coverage eml. + * + * @param {string} geographicDescription + * @param {BoundingBox} boundingBox + * @param {*} polygonRows + * @return {Eml} + */ +const getGeographicCoverageEML = (geographicDescription: string, boundingBox: any, polygonRows: any): Eml => { + const geographicCoverage: Eml = { + geographicDescription: geographicDescription, + boundingCoordinates: { + westBoundingCoordinate: boundingBox.st_xmin, + eastBoundingCoordinate: boundingBox.st_xmax, + northBoundingCoordinate: boundingBox.st_ymax, + southBoundingCoordinate: boundingBox.st_ymin + } + }; + geographicCoverage.datasetGPolygon = []; + polygonRows.rows.forEach(function (row: any, i: number) { + geographicCoverage.datasetGPolygon[i] = { datasetGPolygonOuterGRing: { gRingPoint: [] } }; + row.points.forEach(function (point: any, g: number) { + geographicCoverage.datasetGPolygon[i].datasetGPolygonOuterGRing.gRingPoint[g] = { + gRingLatitude: point[0], + gRingLongitude: point[1] + }; + }); + }); + + return geographicCoverage; +}; + +/** + * Return funding source eml. + * + * @param {any[]} fundingSourceRows + * @return {Eml} + */ +const getFundingEML = (fundingSourceRows: any[]): Eml => { + const funding: Eml = { section: [] }; + + fundingSourceRows.forEach(function (row: any, i: number) { + funding.section[i] = { title: 'Funding Source', para: row.funding_source_name }; + funding.section[i].section = { + title: 'Investment Action Category', + para: row.investment_action_category_name, + section: [ + { title: 'Funding Source Project ID', para: row.funding_source_project_id }, + { title: 'Funding Amount', para: row.funding_amount }, + { title: 'Funding Start Date', para: new Date(row.funding_start_date).toISOString().split('T')[0] }, + { title: 'Funding End Date', para: new Date(row.funding_end_date).toISOString().split('T')[0] } + ] + }; + }); + + return funding; +}; + +type StringIfNull = T extends null | undefined ? string : T; + +/** + * Return default message if value not provided. + * + * @param {string} valueToCheck + * @param {IDBConnection} connection + * @return {string | number} + */ +const checkProvided = (valueToCheck: T): StringIfNull => { + // the EML specification requires all fields have values + // fail gracefully by providing standard message that data is not supplied + const notSuppliedMessage = 'Not Supplied'; + + if (valueToCheck === null) { + return notSuppliedMessage as StringIfNull; + } + + return valueToCheck as StringIfNull; +}; + +/** + * Get occurrence submission record associated with data package ID. + * + * @param {number} data_package_id + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getSurveyOccurrenceSubmission = async (dataPackageId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getSurveyOccurrenceSubmissionSQL(dataPackageId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || response.rowCount != 1) { + throw new HTTP400('Failed to acquire distinct survey occurrence submission record'); + } + + return response.rows[0]; +}; + +/** + * Get data package record associated with data package ID. + * + * @param {number} data_package_id + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getDataPackage = async (dataPackageId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getDataPackageSQL(dataPackageId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to acquire data package record'); + } + + return response.rows[0]; +}; + +/** + * Get published survey status. + * + * @param {number} data_package_id + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getPublishedSurveyStatus = async ( + occurrenceSubmissionId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.dwc.getPublishedSurveyStatusSQL(occurrenceSubmissionId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get survey record. + * + * @param {number} surveyId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getSurvey = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getSurveySQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows[0]; +}; + +/** + * Get project record. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProject = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows[0]; +}; + +/** + * Get survey funding source records. + * + * @param {number} surveyId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getSurveyFundingSource = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getSurveyFundingSourceSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows; +}; + +/** + * Get project funding source records. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectFundingSource = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectFundingSourceSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows; +}; + +/** + * Get survey bounding box. + * + * @param {number} surveyId + * @param {IDBConnection} connection + * @return {BoundingBox} {Promise} + */ +export const getSurveyBoundingBox = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getGeometryBoundingBoxSQL(surveyId, 'survey_id', 'survey'); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows[0]; +}; + +/** + * Get project bounding box. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {BoundingBox} {Promise} + */ +export const getProjectBoundingBox = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getGeometryBoundingBoxSQL(projectId, 'project_id', 'project'); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return (await connection.query(sqlStatement.text, sqlStatement.values)).rows[0]; +}; + +/** + * Get survey polygons. + * + * @param {number} surveyId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getSurveyPolygons = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getGeometryPolygonsSQL(surveyId, 'survey_id', 'survey'); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project polygons. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectPolygons = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getGeometryPolygonsSQL(projectId, 'project_id', 'project'); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get focal taxonomic coverage. + * + * @param {number} surveyId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getFocalTaxonomicCoverage = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getTaxonomicCoverageSQL(surveyId, true); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project IUCN conservation data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectIucnConservation = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectIucnConservationSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project stakeholder partnership data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectStakeholderPartnership = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectStakeholderPartnershipSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project activity data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectActivity = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectActivitySQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project climate initiative data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectClimateInitiative = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectClimateInitiativeSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project first nations data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectFirstNations = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectFirstNationsSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project management action data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectManagementActions = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getProjectManagementActionsSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * Get project survey proprietor data. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getSurveyProprietor = async (projectId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.dwc.getSurveyProprietorSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + return connection.query(sqlStatement.text, sqlStatement.values); +}; diff --git a/api/src/paths/dwc/scrape-occurrences.ts b/api/src/paths/dwc/scrape-occurrences.ts index 31449bbed0..3997da799c 100644 --- a/api/src/paths/dwc/scrape-occurrences.ts +++ b/api/src/paths/dwc/scrape-occurrences.ts @@ -1,19 +1,29 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE } from '../../constants/roles'; import { getDBConnection, IDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; +import { HTTP400 } from '../../errors/custom-error'; import { PostOccurrence } from '../../models/occurrence-create'; -import { postOccurrenceSQL } from '../../queries/occurrence/occurrence-create-queries'; +import { queries } from '../../queries/queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; import { getLogger } from '../../utils/logger'; import { DWCArchive } from '../../utils/media/dwc/dwc-archive-file'; -import { logRequest } from '../../utils/path-utils'; import { getOccurrenceSubmission, getS3File, prepDWCArchive, sendResponse } from './validate'; const defaultLog = getLogger('paths/dwc/scrape-occurrences'); export const POST: Operation = [ - logRequest('paths/dwc/scrape-occurrences', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), getOccurrenceSubmission(), getSubmissionOutputS3Key(), getS3File(), @@ -27,7 +37,7 @@ POST.apiDoc = { tags: ['scrape', 'occurrence'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -38,6 +48,9 @@ POST.apiDoc = { type: 'object', required: ['occurrence_submission_id'], properties: { + project_id: { + type: 'number' + }, occurrence_submission_id: { description: 'A survey occurrence submission ID', type: 'number', @@ -187,7 +200,7 @@ export const uploadScrapedOccurrence = async ( scrapedOccurrence: PostOccurrence, connection: IDBConnection ) => { - const sqlStatement = postOccurrenceSQL(occurrenceSubmissionId, scrapedOccurrence); + const sqlStatement = queries.occurrence.postOccurrenceSQL(occurrenceSubmissionId, scrapedOccurrence); if (!sqlStatement) { throw new HTTP400('Failed to build SQL post statement'); diff --git a/api/src/paths/dwc/validate.test.ts b/api/src/paths/dwc/validate.test.ts index f085de9a82..59ba5f74a4 100644 --- a/api/src/paths/dwc/validate.test.ts +++ b/api/src/paths/dwc/validate.test.ts @@ -1,16 +1,17 @@ +import { GetObjectOutput } from 'aws-sdk/clients/s3'; import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as validate from './validate'; -import * as db from '../../database/db'; -import * as survey_occurrence_queries from '../../queries/survey/survey-occurrence-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import survey_queries from '../../queries/survey'; import * as file_utils from '../../utils/file-utils'; -import { GetObjectOutput } from 'aws-sdk/clients/s3'; -import { getMockDBConnection } from '../../__mocks__/db'; -import * as media_utils from '../../utils/media/media-utils'; import { ArchiveFile } from '../../utils/media/media-file'; +import * as media_utils from '../../utils/media/media-utils'; +import { getMockDBConnection } from '../../__mocks__/db'; +import * as validate from './validate'; chai.use(sinonChai); @@ -51,14 +52,14 @@ describe('getOccurrenceSubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `occurrence_submission_id`.'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `occurrence_submission_id`.'); } }); it('should throw a 400 error when no sql statement returned for getSurveyOccurrenceSubmissionSQL', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); try { const result = validate.getOccurrenceSubmission(); @@ -66,8 +67,8 @@ describe('getOccurrenceSubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -83,7 +84,7 @@ describe('getOccurrenceSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); try { const result = validate.getOccurrenceSubmission(); @@ -91,8 +92,8 @@ describe('getOccurrenceSubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get survey occurrence submission'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get survey occurrence submission'); } }); @@ -112,7 +113,7 @@ describe('getOccurrenceSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); const result = validate.getOccurrenceSubmission(); await result(sampleReq, (null as unknown) as any, nextSpy as any); @@ -138,8 +139,8 @@ describe('getS3File', () => { await result(updatedSampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to get file from S3'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to get file from S3'); } }); diff --git a/api/src/paths/dwc/validate.ts b/api/src/paths/dwc/validate.ts index 6376873dd3..f6cfb3d2c3 100644 --- a/api/src/paths/dwc/validate.ts +++ b/api/src/paths/dwc/validate.ts @@ -1,14 +1,10 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE } from '../../constants/roles'; import { getDBConnection, IDBConnection } from '../../database/db'; -import { HTTP400, HTTP500 } from '../../errors/CustomError'; -import { - getSurveyOccurrenceSubmissionSQL, - insertOccurrenceSubmissionMessageSQL, - insertOccurrenceSubmissionStatusSQL, - updateSurveyOccurrenceSubmissionSQL -} from '../../queries/survey/survey-occurrence-queries'; +import { HTTP400, HTTP500 } from '../../errors/custom-error'; +import { queries } from '../../queries/queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; import { getFileFromS3 } from '../../utils/file-utils'; import { getLogger } from '../../utils/logger'; import { ICsvState, IHeaderError, IRowError } from '../../utils/media/csv/csv-file'; @@ -16,12 +12,21 @@ import { DWCArchive } from '../../utils/media/dwc/dwc-archive-file'; import { ArchiveFile, IMediaState } from '../../utils/media/media-file'; import { parseUnknownMedia } from '../../utils/media/media-utils'; import { ValidationSchemaParser } from '../../utils/media/validation/validation-schema-parser'; -import { logRequest } from '../../utils/path-utils'; const defaultLog = getLogger('paths/dwc/validate'); export const POST: Operation = [ - logRequest('paths/dwc/validate', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), getOccurrenceSubmission(), getOccurrenceSubmissionInputS3Key(), getS3File(), @@ -31,7 +36,7 @@ export const POST: Operation = [ getValidationRules(), validateDWCArchive(), persistValidationResults({ initialSubmissionStatusType: 'Darwin Core Validated' }), - updateOccurrenceSubmission, + updateOccurrenceSubmission(), sendResponse() ]; @@ -41,7 +46,7 @@ export const getValidateAPIDoc = (basicDescription: string, successDescription: tags: tags, security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -50,8 +55,11 @@ export const getValidateAPIDoc = (basicDescription: string, successDescription: 'application/json': { schema: { type: 'object', - required: ['occurrence_submission_id'], + required: ['project_id', 'occurrence_submission_id'], properties: { + project_id: { + type: 'number' + }, occurrence_submission_id: { description: 'A survey occurrence submission ID', type: 'number', @@ -88,7 +96,7 @@ export const getValidateAPIDoc = (basicDescription: string, successDescription: $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -121,7 +129,7 @@ export function getOccurrenceSubmission(): RequestHandler { } try { - const sqlStatement = getSurveyOccurrenceSubmissionSQL(occurrenceSubmissionId); + const sqlStatement = queries.survey.getSurveyOccurrenceSubmissionSQL(occurrenceSubmissionId); if (!sqlStatement) { throw new HTTP400('Failed to build SQL get statement'); @@ -131,6 +139,8 @@ export function getOccurrenceSubmission(): RequestHandler { const response = await connection.query(sqlStatement.text, sqlStatement.values); + await connection.commit(); + if (!response || !response.rows.length) { throw new HTTP400('Failed to get survey occurrence submission'); } @@ -449,7 +459,7 @@ export const insertSubmissionStatus = async ( submissionStatusType: string, connection: IDBConnection ): Promise => { - const sqlStatement = insertOccurrenceSubmissionStatusSQL(occurrenceSubmissionId, submissionStatusType); + const sqlStatement = queries.survey.insertOccurrenceSubmissionStatusSQL(occurrenceSubmissionId, submissionStatusType); if (!sqlStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -482,7 +492,7 @@ export const insertSubmissionMessage = async ( errorCode: string, connection: IDBConnection ): Promise => { - const sqlStatement = insertOccurrenceSubmissionMessageSQL( + const sqlStatement = queries.survey.insertOccurrenceSubmissionMessageSQL( submissionStatusId, submissionMessageType, message, @@ -515,7 +525,11 @@ export const updateSurveyOccurrenceSubmissionWithOutputKey = async ( outputKey: string, connection: IDBConnection ): Promise => { - const updateSqlStatement = updateSurveyOccurrenceSubmissionSQL({ submissionId, outputFileName, outputKey }); + const updateSqlStatement = queries.survey.updateSurveyOccurrenceSubmissionSQL({ + submissionId, + outputFileName, + outputKey + }); if (!updateSqlStatement) { throw new HTTP400('Failed to build SQL update statement'); diff --git a/api/src/paths/dwc/view-occurrences.test.ts b/api/src/paths/dwc/view-occurrences.test.ts index e7d842501b..07f78bf942 100644 --- a/api/src/paths/dwc/view-occurrences.test.ts +++ b/api/src/paths/dwc/view-occurrences.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as view_occurrences from './view-occurrences'; -import * as db from '../../database/db'; -import * as occurrence_view_queries from '../../queries/occurrence/occurrence-view-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import occurrence_queries from '../../queries/occurrence'; import { getMockDBConnection } from '../../__mocks__/db'; +import * as view_occurrences from './view-occurrences'; chai.use(sinonChai); @@ -50,8 +51,10 @@ describe('getOccurrencesForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body param `occurrence_submission_id`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Missing required request body param `occurrence_submission_id`' + ); } }); @@ -63,7 +66,7 @@ describe('getOccurrencesForView', () => { } }); - sinon.stub(occurrence_view_queries, 'getOccurrencesForViewSQL').returns(null); + sinon.stub(occurrence_queries, 'getOccurrencesForViewSQL').returns(null); try { const result = view_occurrences.getOccurrencesForView(); @@ -75,8 +78,8 @@ describe('getOccurrencesForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get occurrences for view statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get occurrences for view statement'); } }); @@ -95,7 +98,7 @@ describe('getOccurrencesForView', () => { query: mockQuery }); - sinon.stub(occurrence_view_queries, 'getOccurrencesForViewSQL').returns(SQL`something`); + sinon.stub(occurrence_queries, 'getOccurrencesForViewSQL').returns(SQL`something`); try { const result = view_occurrences.getOccurrencesForView(); @@ -107,8 +110,8 @@ describe('getOccurrencesForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get occurrences view data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get occurrences view data'); } }); @@ -140,7 +143,7 @@ describe('getOccurrencesForView', () => { query: mockQuery }); - sinon.stub(occurrence_view_queries, 'getOccurrencesForViewSQL').returns(SQL`something`); + sinon.stub(occurrence_queries, 'getOccurrencesForViewSQL').returns(SQL`something`); const result = view_occurrences.getOccurrencesForView(); diff --git a/api/src/paths/dwc/view-occurrences.ts b/api/src/paths/dwc/view-occurrences.ts index 13dbcf126c..23da6e2ac2 100644 --- a/api/src/paths/dwc/view-occurrences.ts +++ b/api/src/paths/dwc/view-occurrences.ts @@ -1,23 +1,36 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE } from '../../constants/roles'; import { getDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { getLogger } from '../../utils/logger'; -import { logRequest } from '../../utils/path-utils'; -import { getOccurrencesForViewSQL } from '../../queries/occurrence/occurrence-view-queries'; +import { HTTP400 } from '../../errors/custom-error'; import { GetOccurrencesViewData } from '../../models/occurrence-view'; +import { queries } from '../../queries/queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('paths/dwc/view-occurrences'); -export const POST: Operation = [logRequest('paths/dwc/view-occurrences', 'POST'), getOccurrencesForView()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), + getOccurrencesForView() +]; POST.apiDoc = { description: 'Get occurrence spatial and metadata, for view-only purposes.', tags: ['occurrences'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -28,6 +41,9 @@ POST.apiDoc = { type: 'object', required: ['occurrence_submission_id'], properties: { + project_id: { + type: 'number' + }, occurrence_submission_id: { description: 'A survey occurrence submission ID', type: 'number', @@ -45,8 +61,8 @@ POST.apiDoc = { 'application/json': { schema: { title: 'Occurrences spatial and metadata response object, for view purposes', - type: 'object', - properties: {} + type: 'array', + items: {} } } } @@ -58,7 +74,7 @@ POST.apiDoc = { $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -85,7 +101,7 @@ export function getOccurrencesForView(): RequestHandler { try { await connection.open(); - const sqlStatement = getOccurrencesForViewSQL(Number(req.body.occurrence_submission_id)); + const sqlStatement = queries.occurrence.getOccurrencesForViewSQL(Number(req.body.occurrence_submission_id)); if (!sqlStatement) { throw new HTTP400('Failed to build SQL get occurrences for view statement'); diff --git a/api/src/paths/gcnotify/send.test.ts b/api/src/paths/gcnotify/send.test.ts new file mode 100644 index 0000000000..c80f4a4d0c --- /dev/null +++ b/api/src/paths/gcnotify/send.test.ts @@ -0,0 +1,207 @@ +import axios from 'axios'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { HTTPError } from '../../errors/custom-error'; +import { getRequestHandlerMocks } from '../../__mocks__/db'; +import * as notify from './send'; + +chai.use(sinonChai); + +describe('gcnotify', () => { + describe('sendNotification', () => { + const env = Object.assign({}, process.env); + afterEach(() => { + sinon.restore(); + process.env = env; + }); + + const sampleReq = { + params: { + userId: '1' + }, + body: { + recipient: { emailAddress: 'test@email.com', phoneNumber: null, userId: null }, + message: { + header: 'Hello TEST,', + body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', + body2: 'Your request to become an ((request_type)) was received on ((request_date)).', + footer: 'We will contact you after your request has been reviewed by a member of our team.' + } + } + } as any; + + const sampleRes = { + data: { + content: { item: 'object' }, + id: 'string', + reference: 'string', + scheduled_for: 'string', + template: { item: 'object' }, + uri: 'string' + } + }; + + it('should throw a 400 error when no req body', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = null; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param: body'); + } + }); + + it('should throw a 400 error when no recipient', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, recipient: null }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: recipient'); + } + }); + + it('should throw a 400 error when no message', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, message: null }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: message'); + } + }); + + it('should throw a 400 error when no message.header', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, header: null } }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: message.header'); + } + }); + + it('should throw a 400 error when no message.body1', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, body1: null } }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: message.body1'); + } + }); + + it('should throw a 400 error when no message.body2', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, body2: null } }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: message.body2'); + } + }); + + it('should throw a 400 error when no message.footer', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { ...sampleReq.body, message: { ...sampleReq.body.message, footer: null } }; + + try { + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: message.footer'); + } + }); + + it('sends email notification and returns 200 on success', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = sampleReq.body; + process.env.GCNOTIFY_SECRET_API_KEY = 'temp'; + + const sendEmailGCNotification = sinon.stub(axios, 'post').resolves(sampleRes); + + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(sendEmailGCNotification).to.have.been.calledOnce; + expect(mockRes.statusValue).to.equal(200); + }); + + it('sends sms notification and returns 200 on success', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = sampleReq.params; + mockReq.body = { + recipient: { emailAddress: null, phoneNumber: 2501231231, userId: null }, + message: { + header: 'Hello TEST,', + body1: 'This is a message from the Species Inventory Management System (((env))) ((url)).', + body2: 'Your request to become an ((request_type)) was received on ((request_date)).', + footer: 'We will contact you after your request has been reviewed by a member of our team.' + } + }; + process.env.GCNOTIFY_SECRET_API_KEY = 'temp'; + + const sendPhoneNumberGCNotification = sinon.stub(axios, 'post').resolves(sampleRes); + + const requestHandler = notify.sendNotification(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(sendPhoneNumberGCNotification).to.have.been.calledOnce; + expect(mockRes.statusValue).to.equal(200); + }); + }); +}); diff --git a/api/src/paths/gcnotify/send.ts b/api/src/paths/gcnotify/send.ts new file mode 100644 index 0000000000..451d3f94d2 --- /dev/null +++ b/api/src/paths/gcnotify/send.ts @@ -0,0 +1,204 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { HTTP400 } from '../../errors/custom-error'; +import { IgcNotifyPostReturn } from '../../models/gcnotify'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { GCNotifyService } from '../../services/gcnotify-service'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('paths/gcnotify'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + sendNotification() +]; + +POST.apiDoc = { + description: 'Send notification to defined recipient', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + requestBody: { + description: 'Send notification to given recipient', + content: { + 'application/json': { + schema: { + title: 'User Response Object', + type: 'object', + required: ['recipient', 'message'], + properties: { + recipient: { + type: 'object', + oneOf: [ + { + required: ['emailAddress'] + }, + { + required: ['phoneNumber'] + }, + { + required: ['userId'] + } + ], + properties: { + emailAddress: { + type: 'string' + }, + phoneNumber: { + type: 'string' + }, + userId: { + type: 'number' + } + } + }, + message: { + type: 'object', + required: ['subject', 'header', 'body1', 'body2', 'footer'], + properties: { + subject: { + type: 'string' + }, + header: { + type: 'string' + }, + body1: { + type: 'string' + }, + body2: { + type: 'string' + }, + footer: { + type: 'string' + } + } + } + } + } + } + } + }, + responses: { + 200: { + description: 'GC Notify Response', + content: { + 'application/json': { + schema: { + title: 'User Response Object', + type: 'object', + properties: { + content: { + type: 'object' + }, + id: { + type: 'string' + }, + reference: { + type: 'string' + }, + scheduled_for: { + type: 'string' + }, + template: { + type: 'object' + }, + uri: { + type: 'string' + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Send Notification to a recipient. + * + * @returns {RequestHandler} + */ +export function sendNotification(): RequestHandler { + return async (req, res) => { + const recipient = req.body?.recipient || null; + const message = req.body?.message || null; + + if (!req.body) { + throw new HTTP400('Missing required param: body'); + } + + if (!recipient) { + throw new HTTP400('Missing required body param: recipient'); + } + + if (!message) { + throw new HTTP400('Missing required body param: message'); + } + + if (!message.header) { + throw new HTTP400('Missing required body param: message.header'); + } + + if (!message.body1) { + throw new HTTP400('Missing required body param: message.body1'); + } + + if (!message.body2) { + throw new HTTP400('Missing required body param: message.body2'); + } + + if (!message.footer) { + throw new HTTP400('Missing required body param: message.footer'); + } + + try { + const gcnotifyService = new GCNotifyService(); + let response = {} as IgcNotifyPostReturn; + + if (recipient.emailAddress) { + response = await gcnotifyService.sendEmailGCNotification(recipient.emailAddress, message); + } + + if (recipient.phoneNumber) { + response = await gcnotifyService.sendPhoneNumberGCNotification(recipient.phoneNumber, message); + } + + if (recipient.userId) { + defaultLog.error({ label: 'send gcnotify', message: 'email and sms from Id not implemented yet' }); + } + + return res.status(200).json(response); + } catch (error) { + defaultLog.error({ label: 'send gcnotify', message: 'error', error }); + throw error; + } + }; +} diff --git a/api/src/paths/logger.test.ts b/api/src/paths/logger.test.ts new file mode 100644 index 0000000000..d4bf107e56 --- /dev/null +++ b/api/src/paths/logger.test.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; +import { describe } from 'mocha'; +import { HTTPError } from '../errors/custom-error'; +import * as logger from './logger'; + +describe('logger', () => { + let actualStatus: any = null; + let actualResult: any = null; + + const sampleRes = { + status: (status: any) => { + actualStatus = status; + + return { + send: (response: any) => { + actualResult = response; + } + }; + } + } as any; + + const sampleNext = () => { + // do nothing + }; + + describe('updateLoggerLevel', () => { + it('should throw a 400 error when `level` query param is missing', async () => { + const operation = logger.updateLoggerLevel(); + + const sampleReq = { + query: {} + } as any; + + try { + await operation(sampleReq, sampleRes, sampleNext); + + expect.fail(); + } catch (error) { + expect((error as HTTPError).status).to.equal(400); + expect((error as HTTPError).message).to.equal('Missing required query param `level`'); + } + }); + + it('should return 200 on success', async () => { + const operation = logger.updateLoggerLevel(); + + const sampleReq = { + query: { + level: 'info' + } + } as any; + + await operation(sampleReq, sampleRes, sampleNext); + + expect(actualStatus).to.eql(200); + expect(actualResult).to.eql(undefined); + }); + }); +}); diff --git a/api/src/paths/logger.ts b/api/src/paths/logger.ts new file mode 100644 index 0000000000..676d2554a0 --- /dev/null +++ b/api/src/paths/logger.ts @@ -0,0 +1,76 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../constants/roles'; +import { HTTP400 } from '../errors/custom-error'; +import { authorizeRequestHandler } from '../request-handlers/security/authorization'; +import { setLogLevel, WinstonLogLevel, WinstonLogLevels } from '../utils/logger'; + +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + updateLoggerLevel() +]; + +GET.apiDoc = { + description: "Update the log level for the API's default logger", + tags: ['misc'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'query', + name: 'level', + schema: { + description: 'Log levels, from least logging to most logging', + type: 'string', + enum: [...WinstonLogLevels] + }, + required: true + } + ], + responses: { + 200: { + description: 'OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get api version information. + * + * @returns {RequestHandler} + */ +export function updateLoggerLevel(): RequestHandler { + return (req, res) => { + if (!req.query?.level) { + throw new HTTP400('Missing required query param `level`'); + } + + setLogLevel(req.query.level as WinstonLogLevel); + + res.status(200).send(); + }; +} diff --git a/api/src/paths/permit/create-no-sampling.test.ts b/api/src/paths/permit/create-no-sampling.test.ts index dd35c7f27a..94dac152d7 100644 --- a/api/src/paths/permit/create-no-sampling.test.ts +++ b/api/src/paths/permit/create-no-sampling.test.ts @@ -2,213 +2,59 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as create_no_sampling from './create-no-sampling'; -import * as permit_create_queries from '../../queries/permit/permit-create-queries'; import * as db from '../../database/db'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../__mocks__/db'; +import { HTTPError } from '../../errors/custom-error'; +import { PermitService } from '../../services/permit-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import { createNoSamplePermits } from './create-no-sampling'; chai.use(sinonChai); describe('create-no-sampling', () => { - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - body: { - coordinator: { - first_name: 'first', - last_name: 'last', - email_address: 'email@example.com', - coordinator_agency: 'agency', - share_contact_details: true - }, - permit: { - permits: [ - { - permit_number: 'number', - permit_type: 'type' - } - ] - } - } - } as any; - - let actualResult = { - ids: null - }; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - describe('createNoSamplePermits', () => { afterEach(() => { sinon.restore(); }); - it('should throw a 400 error when no permit passed in request body', async () => { + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - try { - const result = create_no_sampling.createNoSamplePermits(); + sinon.stub(PermitService.prototype, 'createNoSamplePermits').rejects(new Error('a test error')); - await result( - { ...sampleReq, body: { ...sampleReq.body, permit: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing request body param `permit`'); - } - }); - - it('should throw a 400 error when no coordinator passed in request body', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); try { - const result = create_no_sampling.createNoSamplePermits(); + const requestHandler = createNoSamplePermits(); - await result( - { ...sampleReq, body: { ...sampleReq.body, coordinator: null } }, - (null as unknown) as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing request body param `coordinator`'); + expect(dbConnectionObj.rollback).to.have.been.called; + expect(dbConnectionObj.release).to.have.been.called; + expect((actualError as HTTPError).message).to.equal('a test error'); } }); - it('should return the inserted ids on success', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(create_no_sampling, 'insertNoSamplePermit').resolves(20); + it('creates a new non sample permit', async () => { + const dbConnectionObj = getMockDBConnection(); - const result = create_no_sampling.createNoSamplePermits(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult.ids).to.eql([20]); - }); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - it('should throw an error when a failure occurs', async () => { - const expectedError = new Error('cannot process request'); + sinon.stub(PermitService.prototype, 'createNoSamplePermits').resolves([1]); - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(create_no_sampling, 'insertNoSamplePermit').rejects(expectedError); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); try { - const result = create_no_sampling.createNoSamplePermits(); + const requestHandler = createNoSamplePermits(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); - expect.fail(); + await requestHandler(mockReq, mockRes, mockNext); } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); + expect.fail(); } - }); - }); -}); - -describe('insertNoSamplePermit', () => { - afterEach(() => { - sinon.restore(); - }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - - const permitData = { - permit_number: 'number', - permit_type: 'type' - }; - - const coordinatorData = { - first_name: 'first', - last_name: 'last', - email_address: 'email@example.com', - coordinator_agency: 'agency', - share_contact_details: true - }; - - it('should throw an error when cannot generate post sql statement', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - sinon.stub(permit_create_queries, 'postPermitNoSamplingSQL').returns(null); - - try { - await create_no_sampling.insertNoSamplePermit(permitData, coordinatorData, dbConnectionObj); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); - } - }); - - it('should throw a HTTP 400 error when failed to insert non-sampling permits cause result is null', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [null] }); - - sinon.stub(permit_create_queries, 'postPermitNoSamplingSQL').returns(SQL`some`); - - try { - await create_no_sampling.insertNoSamplePermit(permitData, coordinatorData, { - ...dbConnectionObj, - query: mockQuery - }); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert non-sampling permit data'); - } - }); - - it('should throw a HTTP 400 error when failed to insert non-sampling permits cause result id is null', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [{ id: null }] }); - - sinon.stub(permit_create_queries, 'postPermitNoSamplingSQL').returns(SQL`some`); - - try { - await create_no_sampling.insertNoSamplePermit(permitData, coordinatorData, { - ...dbConnectionObj, - query: mockQuery - }); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert non-sampling permit data'); - } - }); - - it('should return the result id on success', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [{ id: 12 }] }); - - sinon.stub(permit_create_queries, 'postPermitNoSamplingSQL').returns(SQL`some`); - - const res = await create_no_sampling.insertNoSamplePermit(permitData, coordinatorData, { - ...dbConnectionObj, - query: mockQuery + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ ids: [1] }); }); - - expect(res).to.equal(12); }); }); diff --git a/api/src/paths/permit/create-no-sampling.ts b/api/src/paths/permit/create-no-sampling.ts index 99a3080217..643985536a 100644 --- a/api/src/paths/permit/create-no-sampling.ts +++ b/api/src/paths/permit/create-no-sampling.ts @@ -1,26 +1,39 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; -import { getDBConnection, IDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { IPostPermitNoSampling, PostPermitNoSamplingObject } from '../../models/permit-no-sampling'; -import { PostCoordinatorData } from '../../models/project-create'; -import { PutCoordinatorData } from '../../models/project-update'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; import { permitNoSamplingPostBody, permitNoSamplingResponseBody } from '../../openapi/schemas/permit-no-sampling'; -import { postPermitNoSamplingSQL } from '../../queries/permit/permit-create-queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { PermitService } from '../../services/permit-service'; import { getLogger } from '../../utils/logger'; -import { logRequest } from '../../utils/path-utils'; const defaultLog = getLogger('/api/permit/create-no-sampling'); -export const POST: Operation = [logRequest('/api/permit/create-no-sampling', 'POST'), createNoSamplePermits()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + or: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }, + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + createNoSamplePermits() +]; POST.apiDoc = { description: 'Creates new no sample permit records.', tags: ['no-sample-permit'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -51,7 +64,7 @@ POST.apiDoc = { $ref: '#/components/responses/401' }, 403: { - $ref: '#/components/responses/401' + $ref: '#/components/responses/403' }, 500: { $ref: '#/components/responses/500' @@ -71,24 +84,11 @@ export function createNoSamplePermits(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); - const sanitizedNoSamplePermitPostData = new PostPermitNoSamplingObject(req.body); - - if (!sanitizedNoSamplePermitPostData.permit || !sanitizedNoSamplePermitPostData.permit.permits.length) { - throw new HTTP400('Missing request body param `permit`'); - } - - if (!sanitizedNoSamplePermitPostData.coordinator) { - throw new HTTP400('Missing request body param `coordinator`'); - } - try { await connection.open(); + const permitService = new PermitService(connection); - const result = await Promise.all( - sanitizedNoSamplePermitPostData.permit.permits.map((permit: IPostPermitNoSampling) => - insertNoSamplePermit(permit, sanitizedNoSamplePermitPostData.coordinator, connection) - ) - ); + const result = await permitService.createNoSamplePermits(req.body); await connection.commit(); @@ -102,27 +102,3 @@ export function createNoSamplePermits(): RequestHandler { } }; } - -export const insertNoSamplePermit = async ( - permit: IPostPermitNoSampling, - coordinator: PostCoordinatorData | PutCoordinatorData, - connection: IDBConnection -): Promise => { - const systemUserId = connection.systemUserId(); - - const sqlStatement = postPermitNoSamplingSQL({ ...permit, ...coordinator }, systemUserId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert non-sampling permit data'); - } - - return result.id; -}; diff --git a/api/src/paths/permit/get-no-sampling.test.ts b/api/src/paths/permit/get-no-sampling.test.ts index 96a8d2b8ea..585132107e 100644 --- a/api/src/paths/permit/get-no-sampling.test.ts +++ b/api/src/paths/permit/get-no-sampling.test.ts @@ -2,112 +2,61 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_no_sampling from './get-no-sampling'; import * as db from '../../database/db'; -import * as permit_view_queries from '../../queries/permit/permit-view-queries'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../__mocks__/db'; +import { HTTPError } from '../../errors/custom-error'; +import { PermitService } from '../../services/permit-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import { getNonSamplingPermits } from './get-no-sampling'; chai.use(sinonChai); -describe('getNonSamplingPermits', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {} - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when no sql statement returned for non-sampling permits', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } +describe('get-no-sampling', () => { + describe('getNonSamplingPermits', () => { + afterEach(() => { + sinon.restore(); }); - sinon.stub(permit_view_queries, 'getNonSamplingPermitsSQL').returns(null); - - try { - const result = get_no_sampling.getNonSamplingPermits(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - it('should return non-sampling permits on success', async () => { - const nonSamplingPermits = [ - { - permit_id: 1, - number: '123', - type: 'scientific' - }, - { - permit_id: 2, - number: '12345', - type: 'wildlife' - } - ]; + sinon.stub(PermitService.prototype, 'getNonSamplingPermits').rejects(new Error('a test error')); - const mockQuery = sinon.stub(); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - mockQuery.resolves({ rows: nonSamplingPermits }); + try { + const requestHandler = getNonSamplingPermits(); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.rollback).to.have.been.called; + expect(dbConnectionObj.release).to.have.been.called; + expect((actualError as HTTPError).message).to.equal('a test error'); + } }); - sinon.stub(permit_view_queries, 'getNonSamplingPermitsSQL').returns(SQL`some query`); + it('gets non sample permits', async () => { + const dbConnectionObj = getMockDBConnection(); - const result = get_no_sampling.getNonSamplingPermits(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + sinon + .stub(PermitService.prototype, 'getNonSamplingPermits') + .resolves([{ permit_id: '1', number: '2', type: '3' }]); - expect(actualResult).to.eql(nonSamplingPermits); - }); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - it('should return null when permits response has no rows', async () => { - const mockQuery = sinon.stub(); + try { + const requestHandler = getNonSamplingPermits(); - mockQuery.resolves({ rows: null }); + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql([{ permit_id: '1', number: '2', type: '3' }]); }); - - sinon.stub(permit_view_queries, 'getNonSamplingPermitsSQL').returns(SQL`some query`); - - const result = get_no_sampling.getNonSamplingPermits(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.be.null; }); }); diff --git a/api/src/paths/permit/get-no-sampling.ts b/api/src/paths/permit/get-no-sampling.ts index e533b87999..ac0a7d583e 100644 --- a/api/src/paths/permit/get-no-sampling.ts +++ b/api/src/paths/permit/get-no-sampling.ts @@ -1,23 +1,38 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; import { getDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { getNonSamplingPermitsSQL } from '../../queries/permit/permit-view-queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { PermitService } from '../../services/permit-service'; import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('/api/permit/get-no-sampling'); -export const GET: Operation = [getNonSamplingPermits()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + or: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }, + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getNonSamplingPermits() +]; GET.apiDoc = { description: 'Fetches a list of non-sampling permits.', tags: ['non-sampling-permits'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -47,9 +62,18 @@ GET.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -58,8 +82,6 @@ GET.apiDoc = { export function getNonSamplingPermits(): RequestHandler { return async (req, res) => { - defaultLog.debug({ label: 'Get non-sampling permits list', message: 'params', req_params: req.params }); - const connection = getDBConnection(req['keycloak_token']); try { @@ -67,21 +89,12 @@ export function getNonSamplingPermits(): RequestHandler { const systemUserId = connection.systemUserId(); - const getNonSamplingPermitsSQLStatement = getNonSamplingPermitsSQL(systemUserId); - - if (!getNonSamplingPermitsSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } + const permitService = new PermitService(connection); - const nonSamplingPermitsData = await connection.query( - getNonSamplingPermitsSQLStatement.text, - getNonSamplingPermitsSQLStatement.values - ); + const getNonSamplingPermitsData = await permitService.getNonSamplingPermits(systemUserId); await connection.commit(); - const getNonSamplingPermitsData = (nonSamplingPermitsData && nonSamplingPermitsData.rows) || null; - return res.status(200).json(getNonSamplingPermitsData); } catch (error) { defaultLog.error({ label: 'getNonSamplingPermits', message: 'error', error }); diff --git a/api/src/paths/permit/list.test.ts b/api/src/paths/permit/list.test.ts index 94869aa938..9214217a58 100644 --- a/api/src/paths/permit/list.test.ts +++ b/api/src/paths/permit/list.test.ts @@ -2,116 +2,63 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as list from './list'; import * as db from '../../database/db'; -import * as permit_view_queries from '../../queries/permit/permit-view-queries'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../__mocks__/db'; +import { HTTPError } from '../../errors/custom-error'; +import { PermitService } from '../../services/permit-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import { getAllPermits } from './list'; chai.use(sinonChai); -describe('getAllPermits', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {} - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when no sql statement returned for permits', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } +describe('permit-list', () => { + describe('getAllPermits', () => { + afterEach(() => { + sinon.restore(); }); - sinon.stub(permit_view_queries, 'getAllPermitsSQL').returns(null); - - try { - const result = list.getAllPermits(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - it('should return all permits on success', async () => { - const allPermits = [ - { - id: 1, - number: '123', - type: 'scientific', - coordinator_agency: 'agency', - project_name: 'project 1' - }, - { - id: 2, - number: '12345', - type: 'wildlife', - coordinator_agency: 'agency 2', - project_name: null - } - ]; + sinon.stub(PermitService.prototype, 'getAllPermits').rejects(new Error('a test error')); - const mockQuery = sinon.stub(); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - mockQuery.resolves({ rows: allPermits }); + try { + const requestHandler = getAllPermits(); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.rollback).to.have.been.called; + expect(dbConnectionObj.release).to.have.been.called; + expect((actualError as HTTPError).message).to.equal('a test error'); + } }); - sinon.stub(permit_view_queries, 'getAllPermitsSQL').returns(SQL`some query`); + it('gets non sample permits', async () => { + const dbConnectionObj = getMockDBConnection(); - const result = list.getAllPermits(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + sinon + .stub(PermitService.prototype, 'getAllPermits') + .resolves([{ id: '1', number: '2', type: '3', coordinator_agency: '4', project_name: '5' }]); - expect(actualResult).to.eql(allPermits); - }); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - it('should return null when permits response has no rows', async () => { - const mockQuery = sinon.stub(); + try { + const requestHandler = getAllPermits(); - mockQuery.resolves({ rows: null }); + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql([ + { id: '1', number: '2', type: '3', coordinator_agency: '4', project_name: '5' } + ]); }); - - sinon.stub(permit_view_queries, 'getAllPermitsSQL').returns(SQL`some query`); - - const result = list.getAllPermits(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.be.null; }); }); diff --git a/api/src/paths/permit/list.ts b/api/src/paths/permit/list.ts index 40bf1f2969..42621829b7 100644 --- a/api/src/paths/permit/list.ts +++ b/api/src/paths/permit/list.ts @@ -1,23 +1,38 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; import { getDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { getAllPermitsSQL } from '../../queries/permit/permit-view-queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { PermitService } from '../../services/permit-service'; import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('/api/permits/list'); -export const GET: Operation = [getAllPermits()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + or: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }, + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getAllPermits() +]; GET.apiDoc = { description: 'Fetches a list of all permits by system user id.', tags: ['permits'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -41,7 +56,8 @@ GET.apiDoc = { type: 'string' }, project_name: { - type: 'string' + type: 'string', + nullable: true } } }, @@ -50,9 +66,18 @@ GET.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -61,8 +86,6 @@ GET.apiDoc = { export function getAllPermits(): RequestHandler { return async (req, res) => { - defaultLog.debug({ label: 'Get permits list', message: 'params', req_params: req.params }); - const connection = getDBConnection(req['keycloak_token']); try { @@ -70,18 +93,12 @@ export function getAllPermits(): RequestHandler { const systemUserId = connection.systemUserId(); - const getPermitsSQLStatement = getAllPermitsSQL(systemUserId); + const permitService = new PermitService(connection); - if (!getPermitsSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const permitsData = await connection.query(getPermitsSQLStatement.text, getPermitsSQLStatement.values); + const getPermitsData = await permitService.getAllPermits(systemUserId); await connection.commit(); - const getPermitsData = (permitsData && permitsData.rows) || null; - return res.status(200).json(getPermitsData); } catch (error) { defaultLog.error({ label: 'getAllPermits', message: 'error', error }); diff --git a/api/src/paths/project.test.ts b/api/src/paths/project.test.ts deleted file mode 100644 index 12bbcf73bf..0000000000 --- a/api/src/paths/project.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as project from './project'; -import * as db from '../database/db'; -import * as project_create_queries from '../queries/project/project-create-queries'; -import { getMockDBConnection } from '../__mocks__/db'; - -chai.use(sinonChai); - -describe('createProject', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - body1: { - coordinator: { - first_name: 'John', - last_name: 'Smith', - email_address: 'a@b.com', - coordinator_agency: 'A Rocha Canada', - share_contact_details: 'false' - }, - permit: { permits: [], existing_permits: [] }, - project: { - project_name: 'Tatyana Douglas', - project_type: 2, - project_activities: [], - start_date: '1900-01-01', - end_date: '' - }, - objectives: { objectives: 'an objective', caveats: '' }, - location: { - regions: ['West Coast'], - location_description: '', - geometry: [ - { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [-124.716797, 52.88902] } } - ] - }, - iucn: { classificationDetails: [] }, - funding: { funding_sources: [] }, - partnerships: { indigenous_partnerships: [], stakeholder_partnerships: [] } - }, - params: { - projectId: 1 - } - } as any; - - afterEach(() => { - sinon.restore(); - }); - - it('should throw a 400 error when no request body present', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = project.createProject(); - - await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert project general information data'); - } - }); - - it('should throw a 400 error when no sql statement returned for postProjectSQLStatement', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(project_create_queries, 'postProjectSQL').returns(null); - - try { - const result = project.createProject(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); - } - }); -}); diff --git a/api/src/paths/project.ts b/api/src/paths/project.ts deleted file mode 100644 index cac958c4a9..0000000000 --- a/api/src/paths/project.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getDBConnection, IDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; -import { - IPostExistingPermit, - IPostIUCN, - IPostPermit, - PostFundingSource, - PostProjectObject -} from '../models/project-create'; -import { projectCreatePostRequestObject, projectIdResponseObject } from '../openapi/schemas/project'; -import { associatePermitToProjectSQL } from '../queries/permit/permit-update-queries'; -import { postProjectPermitSQL } from '../queries/permit/permit-create-queries'; -import { - getProjectAttachmentByFileNameSQL, - postProjectAttachmentSQL, - postProjectReportAttachmentSQL, - putProjectAttachmentSQL, - putProjectReportAttachmentSQL -} from '../queries/project/project-attachments-queries'; -import { - postProjectActivitySQL, - postProjectFundingSourceSQL, - postProjectIndigenousNationSQL, - postProjectIUCNSQL, - postProjectSQL, - postProjectStakeholderPartnershipSQL -} from '../queries/project/project-create-queries'; -import { generateS3FileKey } from '../utils/file-utils'; -import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; - -const defaultLog = getLogger('paths/project'); - -export const POST: Operation = [logRequest('paths/project', 'POST'), createProject()]; - -POST.apiDoc = { - description: 'Create a new Project.', - tags: ['project'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - requestBody: { - description: 'Project post request object.', - content: { - 'application/json': { - schema: { - ...(projectCreatePostRequestObject as object) - } - } - } - }, - responses: { - 200: { - description: 'Project response object.', - content: { - 'application/json': { - schema: { - ...(projectIdResponseObject as object) - } - } - } - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -/** - * Creates a new project record. - * - * @returns {RequestHandler} - */ -export function createProject(): RequestHandler { - return async (req, res) => { - const connection = getDBConnection(req['keycloak_token']); - - const sanitizedProjectPostData = new PostProjectObject(req.body); - - try { - const postProjectSQLStatement = postProjectSQL({ - ...sanitizedProjectPostData.project, - ...sanitizedProjectPostData.location, - ...sanitizedProjectPostData.objectives, - ...sanitizedProjectPostData.coordinator - }); - - if (!postProjectSQLStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - let projectId: number; - - try { - await connection.open(); - - // Handle project details - const createProjectResponse = await connection.query( - postProjectSQLStatement.text, - postProjectSQLStatement.values - ); - - const projectResult = - (createProjectResponse && createProjectResponse.rows && createProjectResponse.rows[0]) || null; - - if (!projectResult || !projectResult.id) { - throw new HTTP400('Failed to insert project general information data'); - } - - projectId = projectResult.id; - - const promises: Promise[] = []; - - // Handle funding sources - promises.push( - Promise.all( - sanitizedProjectPostData.funding.funding_sources.map((fundingSource: PostFundingSource) => - insertFundingSource(fundingSource, projectId, connection) - ) - ) - ); - - // Handle indigenous partners - promises.push( - Promise.all( - sanitizedProjectPostData.partnerships.indigenous_partnerships.map((indigenousNationId: number) => - insertIndigenousNation(indigenousNationId, projectId, connection) - ) - ) - ); - - // Handle stakeholder partners - promises.push( - Promise.all( - sanitizedProjectPostData.partnerships.stakeholder_partnerships.map((stakeholderPartner: string) => - insertStakeholderPartnership(stakeholderPartner, projectId, connection) - ) - ) - ); - - // Handle new project permits - promises.push( - Promise.all( - sanitizedProjectPostData.permit.permits.map((permit: IPostPermit) => - insertPermit(permit.permit_number, permit.permit_type, projectId, connection) - ) - ) - ); - - // Handle existing non-sampling permits which are now being associated to a project - promises.push( - Promise.all( - sanitizedProjectPostData.permit.existing_permits.map((existing_permit: IPostExistingPermit) => - associateExistingPermitToProject(existing_permit.permit_id, projectId, connection) - ) - ) - ); - - // Handle project IUCN classifications - promises.push( - Promise.all( - sanitizedProjectPostData.iucn.classificationDetails.map((classificationDetail: IPostIUCN) => - insertClassificationDetail(classificationDetail.subClassification2, projectId, connection) - ) - ) - ); - - // Handle project activities - promises.push( - Promise.all( - sanitizedProjectPostData.project.project_activities.map((activityId: number) => - insertProjectActivity(activityId, projectId, connection) - ) - ) - ); - - await Promise.all(promises); - - await connection.commit(); - } catch (error) { - await connection.rollback(); - throw error; - } - - return res.status(200).json({ id: projectId }); - } catch (error) { - defaultLog.error({ label: 'createProject', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} - -export const insertFundingSource = async ( - fundingSource: PostFundingSource, - project_id: number, - connection: IDBConnection -): Promise => { - const sqlStatement = postProjectFundingSourceSQL(fundingSource, project_id); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project funding data'); - } - - return result.id; -}; - -export const insertIndigenousNation = async ( - indigenousNationId: number, - project_id: number, - connection: IDBConnection -): Promise => { - const sqlStatement = postProjectIndigenousNationSQL(indigenousNationId, project_id); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project first nations partnership data'); - } - - return result.id; -}; - -export const insertStakeholderPartnership = async ( - stakeholderPartner: string, - project_id: number, - connection: IDBConnection -): Promise => { - const sqlStatement = postProjectStakeholderPartnershipSQL(stakeholderPartner, project_id); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project stakeholder partnership data'); - } - - return result.id; -}; - -export const insertPermit = async ( - permitNumber: string, - permitType: string, - projectId: number, - connection: IDBConnection -): Promise => { - const systemUserId = connection.systemUserId(); - - const sqlStatement = postProjectPermitSQL(permitNumber, permitType, projectId, systemUserId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project permit data'); - } - - return result.id; -}; - -export const associateExistingPermitToProject = async ( - permitId: number, - projectId: number, - connection: IDBConnection -): Promise => { - const sqlStatement = associatePermitToProjectSQL(permitId, projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL update statement for associatePermitToProjectSQL'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rowCount) || null; - - if (!result) { - throw new HTTP400('Failed to associate existing permit to project'); - } -}; - -export const insertClassificationDetail = async ( - iucn3_id: number, - project_id: number, - connection: IDBConnection -): Promise => { - const sqlStatement = postProjectIUCNSQL(iucn3_id, project_id); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project IUCN data'); - } - - return result.id; -}; - -export const insertProjectActivity = async ( - activityId: number, - projectId: number, - connection: IDBConnection -): Promise => { - const sqlStatement = postProjectActivitySQL(activityId, projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result || !result.id) { - throw new HTTP400('Failed to insert project activity data'); - } - - return result.id; -}; - -export const upsertProjectAttachment = async ( - file: Express.Multer.File, - projectId: number, - attachmentType: string, - connection: IDBConnection -): Promise => { - const getSqlStatement = getProjectAttachmentByFileNameSQL(projectId, file.originalname); - - if (!getSqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const getResponse = await connection.query(getSqlStatement.text, getSqlStatement.values); - - if (getResponse && getResponse.rowCount > 0) { - const updateSqlStatement = - attachmentType === 'Report' - ? putProjectReportAttachmentSQL(projectId, file.originalname) - : putProjectAttachmentSQL(projectId, file.originalname, attachmentType); - - if (!updateSqlStatement) { - throw new HTTP400('Failed to build SQL update statement'); - } - - const updateResponse = await connection.query(updateSqlStatement.text, updateSqlStatement.values); - const updateResult = (updateResponse && updateResponse.rowCount) || null; - - if (!updateResult) { - throw new HTTP400('Failed to update project attachment data'); - } - - return updateResult; - } - - const key = generateS3FileKey({ projectId: projectId, fileName: file.originalname }); - - const insertSqlStatement = - attachmentType === 'Report' - ? postProjectReportAttachmentSQL(file.originalname, file.size, projectId, key) - : postProjectAttachmentSQL(file.originalname, file.size, attachmentType, projectId, key); - - if (!insertSqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const insertResponse = await connection.query(insertSqlStatement.text, insertSqlStatement.values); - const insertResult = (insertResponse && insertResponse.rows && insertResponse.rows[0]) || null; - - if (!insertResult || !insertResult.id) { - throw new HTTP400('Failed to insert project attachment data'); - } - - return insertResult.id; -}; diff --git a/api/src/paths/project/create.test.ts b/api/src/paths/project/create.test.ts new file mode 100644 index 0000000000..246391f48e --- /dev/null +++ b/api/src/paths/project/create.test.ts @@ -0,0 +1,71 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import { ProjectService } from '../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import { createProject, POST } from './create'; + +chai.use(sinonChai); + +describe('create', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((POST.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('createProject', () => { + afterEach(() => { + sinon.restore(); + }); + + it('creates a new project', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'createProject').resolves(1); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = createProject(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ id: 1 }); + }); + + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'createProject').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = createProject(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.rollback).to.have.been.called; + expect(dbConnectionObj.release).to.have.been.called; + + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/project/create.ts b/api/src/paths/project/create.ts new file mode 100644 index 0000000000..970bfb7453 --- /dev/null +++ b/api/src/paths/project/create.ts @@ -0,0 +1,103 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; +import { PostProjectObject } from '../../models/project-create'; +import { projectCreatePostRequestObject, projectIdResponseObject } from '../../openapi/schemas/project'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { ProjectService } from '../../services/project-service'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('paths/project'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + } + ] + }; + }), + createProject() +]; + +POST.apiDoc = { + description: 'Create a new Project.', + tags: ['project'], + security: [ + { + Bearer: [] + } + ], + requestBody: { + description: 'Project post request object.', + content: { + 'application/json': { + schema: { + ...(projectCreatePostRequestObject as object) + } + } + } + }, + responses: { + 200: { + description: 'Project response object.', + content: { + 'application/json': { + schema: { + ...(projectIdResponseObject as object) + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Creates a new project record. + * + * @returns {RequestHandler} + */ +export function createProject(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + const sanitizedProjectPostData = new PostProjectObject(req.body); + + try { + await connection.open(); + + const projectService = new ProjectService(connection); + + const projectId = await projectService.createProject(sanitizedProjectPostData); + + await connection.commit(); + + return res.status(200).json({ id: projectId }); + } catch (error) { + defaultLog.error({ label: 'createProject', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/list.test.ts b/api/src/paths/project/list.test.ts new file mode 100644 index 0000000000..98b9e9b379 --- /dev/null +++ b/api/src/paths/project/list.test.ts @@ -0,0 +1,108 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import * as authorization from '../../request-handlers/security/authorization'; +import { ProjectService } from '../../services/project-service'; +import { getMockDBConnection } from '../../__mocks__/db'; +import { GET, getProjectList } from './list'; + +chai.use(sinonChai); + +describe('list', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('getProjectList', () => { + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + system_user: { + role_names: [SYSTEM_ROLE.SYSTEM_ADMIN] + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + afterEach(() => { + sinon.restore(); + }); + + it('returns an empty array if no project ids are found', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + sinon.stub(authorization, 'userHasValidRole').returns(true); + sinon.stub(ProjectService.prototype, 'getProjectList').resolves([]); + + const result = getProjectList(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql([]); + }); + + it('returns an array of projects', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + sinon.stub(authorization, 'userHasValidRole').returns(true); + + const mockProject1 = ({ project: { project_id: 1 } } as unknown) as any; + const mockProject2 = ({ project: { project_id: 2 } } as unknown) as any; + + sinon.stub(ProjectService.prototype, 'getProjectList').resolves([mockProject1, mockProject2]); + + const result = getProjectList(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql([mockProject1, mockProject2]); + }); + + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'getProjectList').rejects(new Error('a test error')); + + try { + const requestHandler = getProjectList(); + + await requestHandler(sampleReq, sampleRes as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.release).to.have.been.called; + + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/projects.ts b/api/src/paths/project/list.ts similarity index 54% rename from api/src/paths/projects.ts rename to api/src/paths/project/list.ts index 6b49b531c7..b47fda903e 100644 --- a/api/src/paths/projects.ts +++ b/api/src/paths/project/list.ts @@ -1,26 +1,33 @@ -import { COMPLETION_STATUS } from '../constants/status'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import moment from 'moment'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; -import { projectIdResponseObject } from '../openapi/schemas/project'; -import { getProjectListSQL } from '../queries/project/project-view-queries'; -import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; -import { userHasValidSystemRoles } from '../security/auth-utils'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; +import { projectIdResponseObject } from '../../openapi/schemas/project'; +import { authorizeRequestHandler, userHasValidRole } from '../../request-handlers/security/authorization'; +import { ProjectService } from '../../services/project-service'; +import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('paths/projects'); -export const POST: Operation = [logRequest('paths/projects', 'POST'), getProjectList()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getProjectList() +]; -POST.apiDoc = { +GET.apiDoc = { description: 'Gets a list of projects based on search parameters if passed in.', tags: ['projects'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -121,40 +128,24 @@ POST.apiDoc = { * * @returns {RequestHandler} */ -function getProjectList(): RequestHandler { +export function getProjectList(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); - const filterFields = req.body || null; - try { await connection.open(); + const isUserAdmin = userHasValidRole([SYSTEM_ROLE.SYSTEM_ADMIN], req['system_user']['role_names']); const systemUserId = connection.systemUserId(); - const isUserAdmin = userHasValidSystemRoles([SYSTEM_ROLE.SYSTEM_ADMIN], req['system_user']['role_names']); + const filterFields = req.query || {}; - const getProjectListSQLStatement = getProjectListSQL(isUserAdmin, systemUserId, filterFields); - - if (!getProjectListSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } + const projectService = new ProjectService(connection); - const getProjectListResponse = await connection.query( - getProjectListSQLStatement.text, - getProjectListSQLStatement.values - ); + const projects = await projectService.getProjectList(isUserAdmin, systemUserId, filterFields); await connection.commit(); - let rows: any[] = []; - - if (getProjectListResponse && getProjectListResponse.rows) { - rows = getProjectListResponse.rows; - } - - const result: any[] = _extractProjects(rows); - - return res.status(200).json(result); + return res.status(200).json(projects); } catch (error) { defaultLog.error({ label: 'getProjectList', message: 'error', error }); throw error; @@ -163,38 +154,3 @@ function getProjectList(): RequestHandler { } }; } - -/** - * Extract an array of project data from DB query. - * - * @export - * @param {any[]} rows DB query result rows - * @return {any[]} An array of project data - */ -export function _extractProjects(rows: any[]): any[] { - if (!rows || !rows.length) { - return []; - } - - const projects: any[] = []; - - rows.forEach((row) => { - const project: any = { - id: row.id, - name: row.name, - start_date: row.start_date, - end_date: row.end_date, - coordinator_agency: row.coordinator_agency_name, - publish_status: row.publish_timestamp ? 'Published' : 'Unpublished', - completion_status: - (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || - COMPLETION_STATUS.ACTIVE, - project_type: row.project_type, - permits_list: row.permits_list - }; - - projects.push(project); - }); - - return projects; -} diff --git a/api/src/paths/project/{projectId}/attachments/list.test.ts b/api/src/paths/project/{projectId}/attachments/list.test.ts index b1140f1e79..2eb4b71163 100644 --- a/api/src/paths/project/{projectId}/attachments/list.test.ts +++ b/api/src/paths/project/{projectId}/attachments/list.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as listAttachments from './list'; -import * as db from '../../../../database/db'; -import * as project_attachments_queries from '../../../../queries/project/project-attachments-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import project_queries from '../../../../queries/project'; import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as listAttachments from './list'; chai.use(sinonChai); @@ -45,7 +46,7 @@ describe('lists the project attachments', () => { } }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(null); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(null); try { const result = listAttachments.getAttachments(); @@ -53,8 +54,8 @@ describe('lists the project attachments', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -98,7 +99,7 @@ describe('lists the project attachments', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getAttachments(); @@ -166,7 +167,7 @@ describe('lists the project attachments', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getAttachments(); @@ -207,7 +208,7 @@ describe('lists the project attachments', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getAttachments(); @@ -228,8 +229,8 @@ describe('lists the project attachments', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); }); diff --git a/api/src/paths/project/{projectId}/attachments/list.ts b/api/src/paths/project/{projectId}/attachments/list.ts index bb113bf2e6..db16dfca84 100644 --- a/api/src/paths/project/{projectId}/attachments/list.ts +++ b/api/src/paths/project/{projectId}/attachments/list.ts @@ -1,27 +1,36 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../constants/roles'; import { getDBConnection } from '../../../../database/db'; -import { HTTP400 } from '../../../../errors/CustomError'; +import { HTTP400 } from '../../../../errors/custom-error'; import { GetAttachmentsData } from '../../../../models/project-survey-attachments'; -import { - getProjectAttachmentsSQL, - getProjectReportAttachmentsSQL -} from '../../../../queries/project/project-attachments-queries'; +import { queries } from '../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/attachments/list'); -export const GET: Operation = [getAttachments()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getAttachments() +]; GET.apiDoc = { description: 'Fetches a list of attachments of a project.', tags: ['attachments'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -40,17 +49,35 @@ GET.apiDoc = { content: { 'application/json': { schema: { - type: 'array', - items: { - type: 'object', - properties: { - fileName: { - description: 'The file name of the attachment', - type: 'string' - }, - lastModified: { - description: 'The date the object was last modified', - type: 'string' + type: 'object', + properties: { + attachmentsList: { + type: 'array', + items: { + type: 'object', + required: ['id', 'fileName', 'fileType', 'lastModified', 'securityToken', 'size'], + properties: { + id: { + type: 'number' + }, + fileName: { + type: 'string' + }, + fileType: { + type: 'string' + }, + lastModified: { + type: 'string' + }, + securityToken: { + description: 'The security token of the attachment', + type: 'string', + nullable: true + }, + size: { + type: 'number' + } + } } } } @@ -78,8 +105,10 @@ export function getAttachments(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getProjectAttachmentsSQLStatement = getProjectAttachmentsSQL(Number(req.params.projectId)); - const getProjectReportAttachmentsSQLStatement = getProjectReportAttachmentsSQL(Number(req.params.projectId)); + const getProjectAttachmentsSQLStatement = queries.project.getProjectAttachmentsSQL(Number(req.params.projectId)); + const getProjectReportAttachmentsSQLStatement = queries.project.getProjectReportAttachmentsSQL( + Number(req.params.projectId) + ); if (!getProjectAttachmentsSQLStatement || !getProjectReportAttachmentsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/attachments/report/upload.test.ts b/api/src/paths/project/{projectId}/attachments/report/upload.test.ts new file mode 100644 index 0000000000..726f4fb84f --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/report/upload.test.ts @@ -0,0 +1,146 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import * as file_utils from '../../../../../utils/file-utils'; +import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as upload from './upload'; + +chai.use(sinonChai); + +describe('uploadMedia', () => { + afterEach(() => { + sinon.restore(); + }); + + const dbConnectionObj = getMockDBConnection(); + + const mockReq = { + keycloak_token: {}, + params: { + projectId: 1, + attachmentId: 2 + }, + files: [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ], + body: { + attachmentType: 'Other' + } + } as any; + + let actualResult: any = null; + + const mockRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + } as any; + + it('should throw an error when projectId is missing', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = upload.uploadMedia(); + + await result( + { ...mockReq, params: { ...mockReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing projectId'); + } + }); + + it('should throw an error when files are missing', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = upload.uploadMedia(); + + await result({ ...mockReq, files: [] }, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); + } + }); + + it('should throw a 400 error when file format incorrect', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + + try { + const result = upload.uploadMedia(); + + await result({ ...mockReq, files: ['file1'] }, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + } + }); + + it('should throw a 400 error when file contains malicious content', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); + sinon.stub(upload, 'upsertProjectReportAttachment').resolves({ id: 1, revision_count: 0, key: 'key' }); + sinon.stub(file_utils, 'scanFileForVirus').resolves(false); + + try { + const result = upload.uploadMedia(); + + await result(mockReq, mockRes as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); + } + }); + + it('should return id and revision_count on success (with username and email) when attachmentType is Other', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); + sinon.stub(upload, 'upsertProjectReportAttachment').resolves({ id: 1, revision_count: 0, key: 'key' }); + + const result = upload.uploadMedia(); + + await result(mockReq, mockRes as any, (null as unknown) as any); + + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); + }); +}); diff --git a/api/src/paths/project/{projectId}/attachments/report/upload.ts b/api/src/paths/project/{projectId}/attachments/report/upload.ts new file mode 100644 index 0000000000..228e0b0a02 --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/report/upload.ts @@ -0,0 +1,329 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../database/db'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { + IReportAttachmentAuthor, + PostReportAttachmentMetadata, + PutReportAttachmentMetadata +} from '../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; +import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../utils/file-utils'; +import { getLogger } from '../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/upload'); + +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + uploadMedia() +]; +POST.apiDoc = { + description: 'Upload a project-specific attachment.', + tags: ['attachment'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + required: true + } + ], + requestBody: { + description: 'Attachment upload post request object.', + content: { + 'multipart/form-data': { + schema: { + type: 'object', + required: ['media', 'attachmentMeta'], + properties: { + media: { + type: 'string', + format: 'binary' + }, + attachmentMeta: { + type: 'object', + required: ['title', 'year_published', 'authors', 'description'], + properties: { + title: { + type: 'string' + }, + year_published: { + type: 'string', + description: + 'Year the report is published. (Note: Content-Type: multipart/form-data requires all parameters to be strings.)' + }, + authors: { + type: 'array', + items: { + type: 'object', + required: ['first_name', 'last_name'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + }, + description: { + type: 'string' + } + } + } + } + } + } + } + }, + responses: { + 200: { + description: 'Attachment upload response.', + content: { + 'application/json': { + schema: { + type: 'object', + description: 'Result object', + required: ['attachmentId', 'revision_count'], + properties: { + attachmentId: { + type: 'number' + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + 401: { + $ref: '#/components/responses/401' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Uploads any media in the request to S3, adding their keys to the request. + * Also adds the metadata to the project_attachment DB table + * Does nothing if no media is present in the request. + * + * + * @returns {RequestHandler} + */ +export function uploadMedia(): RequestHandler { + return async (req, res) => { + const rawMediaArray: Express.Multer.File[] = req.files as Express.Multer.File[]; + + if (!req.params.projectId) { + throw new HTTP400('Missing projectId'); + } + + if (!rawMediaArray || !rawMediaArray.length) { + // no media objects included, skipping media upload step + throw new HTTP400('Missing upload data'); + } + + const rawMediaFile: Express.Multer.File = rawMediaArray[0]; + + defaultLog.debug({ + label: 'uploadMedia', + message: 'file', + file: { ...rawMediaFile, buffer: 'Too big to print' } + }); + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // Scan file for viruses using ClamAV + const virusScanResult = await scanFileForVirus(rawMediaFile); + + if (!virusScanResult) { + throw new HTTP400('Malicious content detected, upload cancelled'); + } + + //Upsert a report attachment + const upsertResult = await upsertProjectReportAttachment( + rawMediaFile, + Number(req.params.projectId), + req.body.attachmentMeta, + connection + ); + + // Upload file to S3 + const metadata = { + filename: rawMediaFile.originalname, + username: (req['auth_payload'] && req['auth_payload'].preferred_username) || '', + email: (req['auth_payload'] && req['auth_payload'].email) || '' + }; + + await uploadFileToS3(rawMediaFile, upsertResult.key, metadata); + + await connection.commit(); + + return res.status(200).json({ attachmentId: upsertResult.id, revision_count: upsertResult.revision_count }); + } catch (error) { + defaultLog.error({ label: 'uploadMedia', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const upsertProjectReportAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentMeta: any, + connection: IDBConnection +): Promise<{ id: number; revision_count: number; key: string }> => { + const getSqlStatement = queries.project.getProjectReportAttachmentByFileNameSQL(projectId, file.originalname); + + if (!getSqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const key = generateS3FileKey({ projectId: projectId, fileName: file.originalname, folder: 'reports' }); + + const getResponse = await connection.query(getSqlStatement.text, getSqlStatement.values); + + let metadata; + let attachmentResult: { id: number; revision_count: number }; + + if (getResponse && getResponse.rowCount > 0) { + // Existing attachment with matching name found, update it + metadata = new PutReportAttachmentMetadata(attachmentMeta); + attachmentResult = await updateProjectReportAttachment(file, projectId, metadata, connection); + } else { + // No matching attachment found, insert new attachment + metadata = new PostReportAttachmentMetadata(attachmentMeta); + attachmentResult = await insertProjectReportAttachment( + file, + projectId, + new PostReportAttachmentMetadata(attachmentMeta), + key, + connection + ); + } + + // Delete any existing attachment author records + await deleteProjectReportAttachmentAuthors(attachmentResult.id, connection); + + const promises = []; + + // Insert any new attachment author records + promises.push( + metadata.authors.map((author) => insertProjectReportAttachmentAuthor(attachmentResult.id, author, connection)) + ); + + await Promise.all(promises); + + return { ...attachmentResult, key }; +}; + +export const insertProjectReportAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentMeta: PostReportAttachmentMetadata, + key: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.project.postProjectReportAttachmentSQL( + file.originalname, + file.size, + projectId, + key, + attachmentMeta + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to insert project attachment data'); + } + + return response.rows[0]; +}; + +export const updateProjectReportAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentMeta: PutReportAttachmentMetadata, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.project.putProjectReportAttachmentSQL(projectId, file.originalname, attachmentMeta); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to update project attachment data'); + } + + return response.rows[0]; +}; + +export const deleteProjectReportAttachmentAuthors = async ( + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.project.deleteProjectReportAttachmentAuthorsSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete attachment report authors statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP400('Failed to delete attachment report authors records'); + } +}; + +export const insertProjectReportAttachmentAuthor = async ( + attachmentId: number, + author: IReportAttachmentAuthor, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.project.insertProjectReportAttachmentAuthorSQL(attachmentId, author); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert attachment report author statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to insert attachment report author record'); + } +}; diff --git a/api/src/paths/project/{projectId}/attachments/upload.test.ts b/api/src/paths/project/{projectId}/attachments/upload.test.ts index 3ac7459325..966f8cabef 100644 --- a/api/src/paths/project/{projectId}/attachments/upload.test.ts +++ b/api/src/paths/project/{projectId}/attachments/upload.test.ts @@ -2,11 +2,11 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as upload from './upload'; -import * as project from '../../../project'; import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; import * as file_utils from '../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as upload from './upload'; chai.use(sinonChai); @@ -17,7 +17,7 @@ describe('uploadMedia', () => { const dbConnectionObj = getMockDBConnection(); - const sampleReq = { + const mockReq = { keycloak_token: {}, params: { projectId: 1, @@ -32,14 +32,12 @@ describe('uploadMedia', () => { size: 340 } ], - body: { - attachmentType: 'type' - } + body: {} } as any; let actualResult: any = null; - const sampleRes = { + const mockRes = { status: () => { return { json: (result: any) => { @@ -47,25 +45,7 @@ describe('uploadMedia', () => { } }; } - }; - - it('should throw an error when attachmentType is missing', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - try { - const result = upload.uploadMedia(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, attachmentType: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing attachment file type'); - } - }); + } as any; it('should throw an error when projectId is missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); @@ -74,14 +54,14 @@ describe('uploadMedia', () => { const result = upload.uploadMedia(); await result( - { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + { ...mockReq, params: { ...mockReq.params, projectId: null } }, (null as unknown) as any, (null as unknown) as any ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing projectId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing projectId'); } }); @@ -91,11 +71,11 @@ describe('uploadMedia', () => { try { const result = upload.uploadMedia(); - await result({ ...sampleReq, files: [] }, (null as unknown) as any, (null as unknown) as any); + await result({ ...mockReq, files: [] }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing upload data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); } }); @@ -112,11 +92,11 @@ describe('uploadMedia', () => { try { const result = upload.uploadMedia(); - await result({ ...sampleReq, files: ['file1'] }, (null as unknown) as any, (null as unknown) as any); + await result({ ...mockReq, files: ['file1'] }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -129,21 +109,21 @@ describe('uploadMedia', () => { }); sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); - sinon.stub(project, 'upsertProjectAttachment').resolves(1); + sinon.stub(upload, 'upsertProjectAttachment').resolves({ id: 1, revision_count: 0, key: 'key' }); sinon.stub(file_utils, 'scanFileForVirus').resolves(false); try { const result = upload.uploadMedia(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await result(mockReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Malicious content detected, upload cancelled'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); } }); - it('should return file key on success (with username and email)', async () => { + it('should return id and revision_count on success (with username and email) with valid parameters', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -153,12 +133,12 @@ describe('uploadMedia', () => { sinon.stub(file_utils, 'scanFileForVirus').resolves(true); sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); - sinon.stub(project, 'upsertProjectAttachment').resolves(1); + sinon.stub(upload, 'upsertProjectAttachment').resolves({ id: 1, revision_count: 0, key: 'key' }); const result = upload.uploadMedia(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await result(mockReq, mockRes as any, (null as unknown) as any); - expect(actualResult).to.eql('1/1/test.txt'); + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); }); }); diff --git a/api/src/paths/project/{projectId}/attachments/upload.ts b/api/src/paths/project/{projectId}/attachments/upload.ts index 4384c4a489..729212fec9 100644 --- a/api/src/paths/project/{projectId}/attachments/upload.ts +++ b/api/src/paths/project/{projectId}/attachments/upload.ts @@ -1,23 +1,36 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../constants/roles'; -import { getDBConnection } from '../../../../database/db'; -import { HTTP400 } from '../../../../errors/CustomError'; +import { ATTACHMENT_TYPE } from '../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { queries } from '../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../utils/file-utils'; import { getLogger } from '../../../../utils/logger'; -import { upsertProjectAttachment } from '../../../project'; const defaultLog = getLogger('/api/project/{projectId}/attachments/upload'); -export const POST: Operation = [uploadMedia()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + uploadMedia() +]; POST.apiDoc = { description: 'Upload a project-specific attachment.', tags: ['attachment'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -33,6 +46,7 @@ POST.apiDoc = { 'multipart/form-data': { schema: { type: 'object', + required: ['media'], properties: { media: { type: 'string', @@ -49,15 +63,32 @@ POST.apiDoc = { content: { 'application/json': { schema: { - type: 'string', - description: 'The S3 unique key for this file.' + type: 'object', + required: ['attachmentId', 'revision_count'], + properties: { + attachmentId: { + type: 'number' + }, + revision_count: { + type: 'number' + } + } } } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -69,7 +100,6 @@ POST.apiDoc = { * Also adds the metadata to the project_attachment DB table * Does nothing if no media is present in the request. * - * TODO: make media handling an extension that can be added to different endpoints/record types * * @returns {RequestHandler} */ @@ -77,15 +107,17 @@ export function uploadMedia(): RequestHandler { return async (req, res) => { const rawMediaArray: Express.Multer.File[] = req.files as Express.Multer.File[]; + if (!req.params.projectId) { + throw new HTTP400('Missing projectId'); + } + if (!rawMediaArray || !rawMediaArray.length) { // no media objects included, skipping media upload step throw new HTTP400('Missing upload data'); } - - if (!req.body || !req.body.attachmentType) { - throw new HTTP400('Missing attachment file type'); + if (!req.body) { + throw new HTTP400('Missing request body'); } - const rawMediaFile: Express.Multer.File = rawMediaArray[0]; defaultLog.debug({ @@ -94,10 +126,6 @@ export function uploadMedia(): RequestHandler { file: { ...rawMediaFile, buffer: 'Too big to print' } }); - if (!req.params.projectId) { - throw new HTTP400('Missing projectId'); - } - const connection = getDBConnection(req['keycloak_token']); try { @@ -110,28 +138,25 @@ export function uploadMedia(): RequestHandler { throw new HTTP400('Malicious content detected, upload cancelled'); } - // Insert file metadata into project_attachment or project_report_attachment table - await upsertProjectAttachment(rawMediaFile, Number(req.params.projectId), req.body.attachmentType, connection); + const upsertResult = await upsertProjectAttachment( + rawMediaFile, + Number(req.params.projectId), + ATTACHMENT_TYPE.OTHER, + connection + ); // Upload file to S3 - const key = generateS3FileKey({ - projectId: Number(req.params.projectId), - fileName: rawMediaFile.originalname - }); - const metadata = { filename: rawMediaFile.originalname, username: (req['auth_payload'] && req['auth_payload'].preferred_username) || '', email: (req['auth_payload'] && req['auth_payload'].email) || '' }; - const result = await uploadFileToS3(rawMediaFile, key, metadata); - - defaultLog.debug({ label: 'uploadMedia', message: 'result', result }); + await uploadFileToS3(rawMediaFile, upsertResult.key, metadata); await connection.commit(); - return res.status(200).json(result.Key); + return res.status(200).json({ attachmentId: upsertResult.id, revision_count: upsertResult.revision_count }); } catch (error) { defaultLog.error({ label: 'uploadMedia', message: 'error', error }); await connection.rollback(); @@ -141,3 +166,81 @@ export function uploadMedia(): RequestHandler { } }; } + +export const upsertProjectAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentType: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number; key: string }> => { + const getSqlStatement = queries.project.getProjectAttachmentByFileNameSQL(projectId, file.originalname); + + if (!getSqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const key = generateS3FileKey({ projectId: projectId, fileName: file.originalname }); + + const getResponse = await connection.query(getSqlStatement.text, getSqlStatement.values); + + let attachmentResult: { id: number; revision_count: number }; + + if (getResponse && getResponse.rowCount > 0) { + // Existing attachment with matching name found, update it + attachmentResult = await updateProjectAttachment(file, projectId, attachmentType, connection); + } else { + // No matching attachment found, insert new attachment + attachmentResult = await insertProjectAttachment(file, projectId, attachmentType, key, connection); + } + + return { ...attachmentResult, key }; +}; + +export const insertProjectAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentType: string, + key: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.project.postProjectAttachmentSQL( + file.originalname, + file.size, + attachmentType, + projectId, + key + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to insert project attachment data'); + } + + return response.rows[0]; +}; + +export const updateProjectAttachment = async ( + file: Express.Multer.File, + projectId: number, + attachmentType: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.project.putProjectAttachmentSQL(projectId, file.originalname, attachmentType); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to update project attachment data'); + } + + return response.rows[0]; +}; diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.test.ts index 3eaa4abdd0..9bf9679478 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.test.ts @@ -1,15 +1,16 @@ +import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as delete_attachment from './delete'; -import * as db from '../../../../../database/db'; -import * as project_attachments_queries from '../../../../../queries/project/project-attachments-queries'; -import * as security_queries from '../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import project_queries from '../../../../../queries/project'; +import security_queries from '../../../../../queries/security'; import * as file_utils from '../../../../../utils/file-utils'; -import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as delete_attachment from './delete'; chai.use(sinonChai); @@ -39,6 +40,9 @@ describe('deleteAttachment', () => { return { json: (result: any) => { actualResult = result; + }, + send: () => { + // do nothing } }; } @@ -57,8 +61,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -75,8 +79,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -93,8 +97,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `attachmentType`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `attachmentType`'); } }); @@ -114,8 +118,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL unsecure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL unsecure record statement'); } }); @@ -140,8 +144,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to unsecure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to unsecure record'); } }); @@ -159,7 +163,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(project_attachments_queries, 'deleteProjectAttachmentSQL').returns(null); + sinon.stub(project_queries, 'deleteProjectAttachmentSQL').returns(null); try { const result = delete_attachment.deleteAttachment(); @@ -167,8 +171,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete project attachment statement'); } }); @@ -179,7 +183,7 @@ describe('deleteAttachment', () => { .onFirstCall() .resolves({ rowCount: 1 }) .onSecondCall() - .resolves({ rows: [{ key: 's3Key' }] }); + .resolves({ rowCount: 1, rows: [{ key: 's3Key' }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -190,7 +194,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(project_attachments_queries, 'deleteProjectAttachmentSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'deleteProjectAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves(null); const result = delete_attachment.deleteAttachment(); @@ -200,7 +204,7 @@ describe('deleteAttachment', () => { expect(actualResult).to.equal(null); }); - it('should return the rowCount response on success when type is not Report', async () => { + it('should return null response on success when type is not Report', async () => { const mockQuery = sinon.stub(); mockQuery @@ -218,23 +222,25 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(project_attachments_queries, 'deleteProjectAttachmentSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'deleteProjectAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves('non null response' as DeleteObjectOutput); const result = delete_attachment.deleteAttachment(); await result(sampleReq, sampleRes as any, (null as unknown) as any); - expect(actualResult).to.equal(1); + expect(actualResult).to.equal(null); }); - it('should return the rowCount response on success when type is Report', async () => { + it('should return null response on success when type is Report', async () => { const mockQuery = sinon.stub(); mockQuery .onFirstCall() .resolves({ rowCount: 1 }) .onSecondCall() + .resolves({ rowCount: 1 }) + .onThirdCall() .resolves({ rows: [{ key: 's3Key' }], rowCount: 1 }); sinon.stub(db, 'getDBConnection').returns({ @@ -246,7 +252,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(project_attachments_queries, 'deleteProjectReportAttachmentSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'deleteProjectReportAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves('non null response' as DeleteObjectOutput); const result = delete_attachment.deleteAttachment(); @@ -257,6 +263,6 @@ describe('deleteAttachment', () => { (null as unknown) as any ); - expect(actualResult).to.equal(1); + expect(actualResult).to.equal(null); }); }); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.ts index b533682dfc..a8bc75f46b 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/delete.ts @@ -1,21 +1,32 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { unsecureAttachmentRecordSQL } from '../../../../../queries/security/security-queries'; +import { ATTACHMENT_TYPE } from '../../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { - deleteProjectAttachmentSQL, - deleteProjectReportAttachmentSQL -} from '../../../../../queries/project/project-attachments-queries'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { deleteFileFromS3 } from '../../../../../utils/file-utils'; import { getLogger } from '../../../../../utils/logger'; import { attachmentApiDocObject } from '../../../../../utils/shared-api-docs'; +import { deleteProjectReportAttachmentAuthors } from '../report/upload'; const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/delete'); -export const POST: Operation = [deleteAttachment()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteAttachment() +]; POST.apiDoc = { ...attachmentApiDocObject( @@ -45,10 +56,40 @@ POST.apiDoc = { content: { 'application/json': { schema: { - type: 'object' + type: 'object', + required: ['attachmentType', 'securityToken'], + properties: { + attachmentType: { + type: 'string' + }, + securityToken: { + type: 'string', + nullable: true + } + } } } } + }, + responses: { + 200: { + description: 'Current attachment type for project attachment deleted' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } } }; @@ -78,32 +119,24 @@ export function deleteAttachment(): RequestHandler { await unsecureProjectAttachmentRecord(req.body.securityToken, req.body.attachmentType, connection); } - // Proceed to delete the attachment record itself - const deleteProjectAttachmentSQLStatement = - req.body.attachmentType === 'Report' - ? deleteProjectReportAttachmentSQL(Number(req.params.attachmentId)) - : deleteProjectAttachmentSQL(Number(req.params.attachmentId)); + let deleteResult: { key: string }; + if (req.body.attachmentType === ATTACHMENT_TYPE.REPORT) { + await deleteProjectReportAttachmentAuthors(Number(req.params.attachmentId), connection); - if (!deleteProjectAttachmentSQLStatement) { - throw new HTTP400('Failed to build SQL delete statement'); + deleteResult = await deleteProjectReportAttachment(Number(req.params.attachmentId), connection); + } else { + deleteResult = await deleteProjectAttachment(Number(req.params.attachmentId), connection); } - const result = await connection.query( - deleteProjectAttachmentSQLStatement.text, - deleteProjectAttachmentSQLStatement.values - ); - - const s3Key = result && result.rows.length && result.rows[0].key; - await connection.commit(); - const deleteFileResult = await deleteFileFromS3(s3Key); + const deleteFileResult = await deleteFileFromS3(deleteResult.key); if (!deleteFileResult) { return res.status(200).json(null); } - return res.status(200).json(result && result.rowCount); + return res.status(200).send(); } catch (error) { defaultLog.error({ label: 'deleteAttachment', message: 'error', error }); await connection.rollback(); @@ -121,8 +154,8 @@ const unsecureProjectAttachmentRecord = async ( ): Promise => { const unsecureRecordSQLStatement = attachmentType === 'Report' - ? unsecureAttachmentRecordSQL('project_report_attachment', securityToken) - : unsecureAttachmentRecordSQL('project_attachment', securityToken); + ? queries.security.unsecureAttachmentRecordSQL('project_report_attachment', securityToken) + : queries.security.unsecureAttachmentRecordSQL('project_attachment', securityToken); if (!unsecureRecordSQLStatement) { throw new HTTP400('Failed to build SQL unsecure record statement'); @@ -137,3 +170,41 @@ const unsecureProjectAttachmentRecord = async ( throw new HTTP400('Failed to unsecure record'); } }; + +export const deleteProjectAttachment = async ( + attachmentId: number, + connection: IDBConnection +): Promise<{ key: string }> => { + const sqlStatement = queries.project.deleteProjectAttachmentSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete project attachment statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to delete project attachment record'); + } + + return response.rows[0]; +}; + +export const deleteProjectReportAttachment = async ( + attachmentId: number, + connection: IDBConnection +): Promise<{ key: string }> => { + const sqlStatement = queries.project.deleteProjectReportAttachmentSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete project report attachment statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to delete project attachment report record'); + } + + return response.rows[0]; +}; diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts index 0d5399a996..27d275e478 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts @@ -2,16 +2,18 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_signed_url from './getSignedUrl'; -import * as db from '../../../../../database/db'; -import * as project_attachments_queries from '../../../../../queries/project/project-attachments-queries'; import SQL from 'sql-template-strings'; +import { ATTACHMENT_TYPE } from '../../../../../constants/attachments'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import project_queries from '../../../../../queries/project'; import * as file_utils from '../../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as get_signed_url from './getSignedUrl'; chai.use(sinonChai); -describe('getSingleAttachmentURL', () => { +describe('getProjectAttachmentSignedURL', () => { afterEach(() => { sinon.restore(); }); @@ -23,6 +25,9 @@ describe('getSingleAttachmentURL', () => { params: { projectId: 1, attachmentId: 2 + }, + query: { + attachmentType: 'Other' } } as any; @@ -42,7 +47,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getProjectAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, @@ -51,8 +56,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -60,7 +65,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getProjectAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, @@ -69,29 +74,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(project_attachments_queries, 'getProjectAttachmentS3KeySQL').returns(null); - - try { - const result = get_signed_url.getSingleAttachmentURL(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -108,36 +92,123 @@ describe('getSingleAttachmentURL', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(project_queries, 'getProjectAttachmentS3KeySQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves(null); - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getProjectAttachmentSignedURL(); await result(sampleReq, sampleRes as any, (null as unknown) as any); expect(actualResult).to.equal(null); }); - it('should return the signed url response on success', async () => { - const mockQuery = sinon.stub(); + describe('non report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); - mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + sinon.stub(project_queries, 'getProjectAttachmentS3KeySQL').returns(null); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + try { + const result = get_signed_url.getProjectAttachmentSignedURL(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build attachment S3 key SQLstatement'); + } }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentS3KeySQL').returns(SQL`some query`); - sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + it('should return the signed url response on success', async () => { + const mockQuery = sinon.stub(); - const result = get_signed_url.getSingleAttachmentURL(); + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(project_queries, 'getProjectAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getProjectAttachmentSignedURL(); - expect(actualResult).to.eql('myurlsigned.com'); + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql('myurlsigned.com'); + }); + }); + + describe('report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(project_queries, 'getProjectReportAttachmentS3KeySQL').returns(null); + + try { + const result = get_signed_url.getProjectAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build report attachment S3 key SQLstatement'); + } + }); + + it('should return the signed url response on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(project_queries, 'getProjectReportAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getProjectAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + + expect(actualResult).to.eql('myurlsigned.com'); + }); }); }); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts index 0cf6b94bc5..85ec18be88 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts @@ -1,26 +1,104 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { getLogger } from '../../../../../utils/logger'; -import { getDBConnection } from '../../../../../database/db'; -import { getProjectAttachmentS3KeySQL } from '../../../../../queries/project/project-attachments-queries'; +import { ATTACHMENT_TYPE } from '../../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../database/db'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getS3SignedURL } from '../../../../../utils/file-utils'; -import { attachmentApiDocObject } from '../../../../../utils/shared-api-docs'; +import { getLogger } from '../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/getSignedUrl'); -export const GET: Operation = [getSingleAttachmentURL()]; - -GET.apiDoc = attachmentApiDocObject( - 'Retrieves the signed url of an attachment in a project by its file name.', - 'GET response containing the signed url of an attachment.' -); +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getProjectAttachmentSignedURL() +]; + +GET.apiDoc = { + description: 'Retrieves the signed url of a project attachment.', + tags: ['attachment'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'query', + name: 'attachmentType', + schema: { + type: 'string', + enum: ['Report', 'Other'] + }, + required: true + } + ], + responses: { + 200: { + description: 'Response containing the signed url of an attachment.', + content: { + 'text/plain': { + schema: { + type: 'string' + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; -export function getSingleAttachmentURL(): RequestHandler { +export function getProjectAttachmentSignedURL(): RequestHandler { return async (req, res) => { - defaultLog.debug({ label: 'Get single attachment url', message: 'params', req_params: req.params }); + defaultLog.debug({ + label: 'getProjectAttachmentSignedURL', + message: 'params', + req_params: req.params, + req_query: req.query, + req_body: req.body + }); if (!req.params.projectId) { throw new HTTP400('Missing required path param `projectId`'); @@ -30,29 +108,33 @@ export function getSingleAttachmentURL(): RequestHandler { throw new HTTP400('Missing required path param `attachmentId`'); } + if (!req.query.attachmentType) { + throw new HTTP400('Missing required query param `attachmentType`'); + } + const connection = getDBConnection(req['keycloak_token']); try { - const getProjectAttachmentS3KeySQLStatement = getProjectAttachmentS3KeySQL( - Number(req.params.projectId), - Number(req.params.attachmentId) - ); - - if (!getProjectAttachmentS3KeySQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const result = await connection.query( - getProjectAttachmentS3KeySQLStatement.text, - getProjectAttachmentS3KeySQLStatement.values - ); + let s3Key; + + if (req.query.attachmentType === ATTACHMENT_TYPE.REPORT) { + s3Key = await getProjectReportAttachmentS3Key( + Number(req.params.projectId), + Number(req.params.attachmentId), + connection + ); + } else { + s3Key = await getProjectAttachmentS3Key( + Number(req.params.projectId), + Number(req.params.attachmentId), + connection + ); + } await connection.commit(); - const s3Key = result && result.rows.length && result.rows[0].key; - const s3SignedUrl = await getS3SignedURL(s3Key); if (!s3SignedUrl) { @@ -61,7 +143,7 @@ export function getSingleAttachmentURL(): RequestHandler { return res.status(200).json(s3SignedUrl); } catch (error) { - defaultLog.error({ label: 'getSingleAttachmentURL', message: 'error', error }); + defaultLog.error({ label: 'getProjectAttachmentSignedURL', message: 'error', error }); await connection.rollback(); throw error; } finally { @@ -69,3 +151,43 @@ export function getSingleAttachmentURL(): RequestHandler { } }; } + +export const getProjectAttachmentS3Key = async ( + projectId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.project.getProjectAttachmentS3KeySQL(projectId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; + +export const getProjectReportAttachmentS3Key = async ( + projectId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.project.getProjectReportAttachmentS3KeySQL(projectId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build report attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.test.ts index cca7c420fc..ffc78b7555 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.test.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as makeSecure from './makeSecure'; -import * as db from '../../../../../database/db'; -import * as security_queries from '../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import security_queries from '../../../../../queries/security'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as makeSecure from './makeSecure'; chai.use(sinonChai); @@ -53,8 +54,8 @@ describe('makeProjectAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -71,8 +72,8 @@ describe('makeProjectAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -89,8 +90,8 @@ describe('makeProjectAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `attachmentType`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `attachmentType`'); } }); @@ -104,8 +105,8 @@ describe('makeProjectAttachmentSecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL secure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL secure record statement'); } }); @@ -125,8 +126,8 @@ describe('makeProjectAttachmentSecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to secure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to secure record'); } }); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.ts index a91a7d6b38..8a48ee0858 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeSecure.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; -import { secureAttachmentRecordSQL } from '../../../../../queries/security/security-queries'; const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/makeSecure'); -export const PUT: Operation = [makeProjectAttachmentSecure()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + makeProjectAttachmentSecure() +]; PUT.apiDoc = { description: 'Make security status of a project attachment secure.', tags: ['attachment', 'security_status'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -43,7 +55,13 @@ PUT.apiDoc = { content: { 'application/json': { schema: { - type: 'object' + type: 'object', + required: ['attachmentType'], + properties: { + attachmentType: { + type: 'string' + } + } } } } @@ -60,9 +78,18 @@ PUT.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -96,12 +123,12 @@ export function makeProjectAttachmentSecure(): RequestHandler { const secureRecordSQLStatement = req.body.attachmentType === 'Report' - ? secureAttachmentRecordSQL( + ? queries.security.secureAttachmentRecordSQL( Number(req.params.attachmentId), 'project_report_attachment', Number(req.params.projectId) ) - : secureAttachmentRecordSQL( + : queries.security.secureAttachmentRecordSQL( Number(req.params.attachmentId), 'project_attachment', Number(req.params.projectId) diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.test.ts index a1d86ea8dd..b2a5c6ae95 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.test.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as makeUnsecure from './makeUnsecure'; -import * as db from '../../../../../database/db'; -import * as security_queries from '../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import security_queries from '../../../../../queries/security'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as makeUnsecure from './makeUnsecure'; chai.use(sinonChai); @@ -54,8 +55,8 @@ describe('makeProjectAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -72,8 +73,8 @@ describe('makeProjectAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -86,8 +87,8 @@ describe('makeProjectAttachmentUnsecure', () => { await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -104,8 +105,8 @@ describe('makeProjectAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -122,8 +123,8 @@ describe('makeProjectAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -137,8 +138,8 @@ describe('makeProjectAttachmentUnsecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL unsecure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL unsecure record statement'); } }); @@ -158,8 +159,8 @@ describe('makeProjectAttachmentUnsecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to unsecure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to unsecure record'); } }); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.ts index 9a4f283f81..808a95aa9f 100644 --- a/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.ts +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/makeUnsecure.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; -import { unsecureAttachmentRecordSQL } from '../../../../../queries/security/security-queries'; const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/makeUnsecure'); -export const PUT: Operation = [makeProjectAttachmentUnsecure()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + makeProjectAttachmentUnsecure() +]; PUT.apiDoc = { description: 'Make security status of a project attachment unsecure.', tags: ['attachment', 'security_status'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -43,7 +55,16 @@ PUT.apiDoc = { content: { 'application/json': { schema: { - type: 'object' + type: 'object', + required: ['attachmentType', 'securityToken'], + properties: { + attachmentType: { + type: 'string' + }, + securityToken: { + type: 'string' + } + } } } } @@ -60,9 +81,18 @@ PUT.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -96,8 +126,8 @@ export function makeProjectAttachmentUnsecure(): RequestHandler { const unsecureRecordSQLStatement = req.body.attachmentType === 'Report' - ? unsecureAttachmentRecordSQL('project_report_attachment', req.body.securityToken) - : unsecureAttachmentRecordSQL('project_attachment', req.body.securityToken); + ? queries.security.unsecureAttachmentRecordSQL('project_report_attachment', req.body.securityToken) + : queries.security.unsecureAttachmentRecordSQL('project_attachment', req.body.securityToken); if (!unsecureRecordSQLStatement) { throw new HTTP400('Failed to build SQL unsecure record statement'); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts new file mode 100644 index 0000000000..63d00b9428 --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts @@ -0,0 +1,161 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../../database/db'; +import { HTTPError } from '../../../../../../errors/custom-error'; +import project_queries from '../../../../../../queries/project'; +import { getMockDBConnection } from '../../../../../../__mocks__/db'; +import * as get_project_metadata from './get'; + +chai.use(sinonChai); + +describe('gets metadata for a project report', () => { + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + body: {}, + params: { + projectId: 1, + attachmentId: 1 + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_project_metadata.getProjectReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no attachmentId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_project_metadata.getProjectReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectReportAttachmentSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(project_queries, 'getProjectReportAttachmentSQL').returns(null); + + try { + const result = get_project_metadata.getProjectReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectReportAuthorsSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(project_queries, 'getProjectReportAuthorsSQL').returns(null); + + try { + const result = get_project_metadata.getProjectReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should return a project report metadata, on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [ + { + attachment_id: 1, + title: 'My report', + update_date: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1' + } + ] + }); + mockQuery.onCall(1).resolves({ rowCount: 1, rows: [{ first_name: 'John', last_name: 'Smith' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(project_queries, 'getProjectReportAttachmentSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectReportAuthorsSQL').returns(SQL`something`); + + const result = get_project_metadata.getProjectReportMetaData(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.be.eql({ + attachment_id: 1, + title: 'My report', + last_modified: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1', + authors: [{ first_name: 'John', last_name: 'Smith' }] + }); + }); +}); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.ts new file mode 100644 index 0000000000..fbcc1b7773 --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/get.ts @@ -0,0 +1,197 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../../../../constants/roles'; +import { getDBConnection } from '../../../../../../database/db'; +import { HTTP400 } from '../../../../../../errors/custom-error'; +import { GetReportAttachmentMetadata } from '../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/getSignedUrl'); + +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getProjectReportMetaData() +]; + +GET.apiDoc = { + description: 'Retrieves the report metadata of a project attachment if filetype is Report.', + tags: ['attachment'], + security: [ + { + Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Response of the report metadata', + content: { + 'application/json': { + schema: { + title: 'metadata get response object', + type: 'object', + required: [ + 'attachment_id', + 'title', + 'last_modified', + 'description', + 'year_published', + 'revision_count', + 'authors' + ], + properties: { + attachment_id: { + description: 'Report metadata attachment id', + type: 'number' + }, + title: { + description: 'Report metadata attachment title ', + type: 'string' + }, + last_modified: { + description: 'Report metadata last modified', + type: 'string' + }, + description: { + description: 'Report metadata description', + type: 'string' + }, + year_published: { + description: 'Report metadata year published', + type: 'number' + }, + revision_count: { + description: 'Report metadata revision count', + type: 'number' + }, + authors: { + description: 'Report metadata author object', + type: 'array', + items: { + type: 'object', + required: ['first_name', 'last_name'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function getProjectReportMetaData(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'getProjectReportMetaData', + message: 'params', + req_params: req.params, + req_query: req.query + }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.attachmentId) { + throw new HTTP400('Missing required path param `attachmentId`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const getProjectReportAttachmentSQLStatement = queries.project.getProjectReportAttachmentSQL( + Number(req.params.projectId), + Number(req.params.attachmentId) + ); + + const getProjectReportAuthorsSQLStatement = queries.project.getProjectReportAuthorsSQL( + Number(req.params.attachmentId) + ); + + if (!getProjectReportAttachmentSQLStatement || !getProjectReportAuthorsSQLStatement) { + throw new HTTP400('Failed to build metadata SQLStatement'); + } + + await connection.open(); + + const reportMetaData = await connection.query( + getProjectReportAttachmentSQLStatement.text, + getProjectReportAttachmentSQLStatement.values + ); + + const reportAuthorsData = await connection.query( + getProjectReportAuthorsSQLStatement.text, + getProjectReportAuthorsSQLStatement.values + ); + + await connection.commit(); + + const getReportMetaData = reportMetaData && reportMetaData.rows[0]; + + const getReportAuthorsData = reportAuthorsData && reportAuthorsData.rows; + + const reportMetaObj = new GetReportAttachmentMetadata(getReportMetaData, getReportAuthorsData); + + return res.status(200).json(reportMetaObj); + } catch (error) { + defaultLog.error({ label: 'getReportMetadata', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.test.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.test.ts new file mode 100644 index 0000000000..1b5e845ccd --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.test.ts @@ -0,0 +1,279 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../../database/db'; +import { HTTPError } from '../../../../../../errors/custom-error'; +import project_queries from '../../../../../../queries/project'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../__mocks__/db'; +import * as update_project_metadata from './update'; + +chai.use(sinonChai); + +describe('updates metadata for a project report', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no attachmentId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + attachmentId: '' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); + } + }); + + it('should throw a 400 error when attachment_type is invalid', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'notAReport', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Invalid body param `attachment_type`'); + } + }); + + it('should update a project report metadata, on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + mockQuery.onCall(1).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); + + it('should throw a 400 error when updateProjectReportAttachmentMetadataSQL returns null', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(project_queries, 'updateProjectReportAttachmentMetadataSQL').returns(null); + + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update attachment report statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when the response is null', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: null + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(project_queries, 'updateProjectReportAttachmentMetadataSQL').returns(SQL`something`); + + const requestHandler = update_project_metadata.updateProjectAttachmentMetadata(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to update attachment report record'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); diff --git a/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.ts b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.ts new file mode 100644 index 0000000000..b0362c7ad4 --- /dev/null +++ b/api/src/paths/project/{projectId}/attachments/{attachmentId}/metadata/update.ts @@ -0,0 +1,212 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { ATTACHMENT_TYPE } from '../../../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../../database/db'; +import { HTTP400 } from '../../../../../../errors/custom-error'; +import { PutReportAttachmentMetadata } from '../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../../../utils/logger'; +import { deleteProjectReportAttachmentAuthors, insertProjectReportAttachmentAuthor } from '../../report/upload'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/metadata/update'); + +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateProjectAttachmentMetadata() +]; + +PUT.apiDoc = { + description: 'Update project attachment metadata.', + tags: ['attachment'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + description: 'Attachment metadata for attachments of type: Report.', + required: ['attachment_type', 'attachment_meta', 'revision_count'], + properties: { + attachment_type: { + type: 'string', + enum: ['Report'] + }, + attachment_meta: { + type: 'object', + required: ['title', 'year_published', 'authors', 'description'], + properties: { + title: { + type: 'string' + }, + year_published: { + type: 'number' + }, + authors: { + type: 'array', + items: { + type: 'object', + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + }, + description: { + type: 'string' + } + } + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Update project attachment metadata OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function updateProjectAttachmentMetadata(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'updateProjectAttachmentMetadata', + message: 'params', + req_params: req.params, + req_body: req.body + }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.attachmentId) { + throw new HTTP400('Missing required path param `attachmentId`'); + } + + if (!Object.values(ATTACHMENT_TYPE).includes(req.body?.attachment_type)) { + throw new HTTP400('Invalid body param `attachment_type`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + if (req.body.attachment_type === ATTACHMENT_TYPE.REPORT) { + const metadata = new PutReportAttachmentMetadata({ + ...req.body.attachment_meta, + revision_count: req.body.revision_count + }); + + // Update the metadata fields of the attachment record + await updateProjectReportAttachmentMetadata( + Number(req.params.projectId), + Number(req.params.attachmentId), + metadata, + connection + ); + + // Delete any existing attachment author records + await deleteProjectReportAttachmentAuthors(Number(req.params.attachmentId), connection); + + const promises = []; + + // Insert any new attachment author records + promises.push( + metadata.authors.map((author) => + insertProjectReportAttachmentAuthor(Number(req.params.attachmentId), author, connection) + ) + ); + + await Promise.all(promises); + } + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateProjectAttachmentMetadata', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +const updateProjectReportAttachmentMetadata = async ( + projectId: number, + attachmentId: number, + metadata: PutReportAttachmentMetadata, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.project.updateProjectReportAttachmentMetadataSQL(projectId, attachmentId, metadata); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update attachment report statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to update attachment report record'); + } +}; diff --git a/api/src/paths/project/{projectId}/delete.test.ts b/api/src/paths/project/{projectId}/delete.test.ts index ee4c451f79..94cf75266a 100644 --- a/api/src/paths/project/{projectId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/delete.test.ts @@ -6,14 +6,13 @@ import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import { SYSTEM_ROLE } from '../../../constants/roles'; import * as db from '../../../database/db'; -import * as project_attachments_queries from '../../../queries/project/project-attachments-queries'; -import * as project_delete_queries from '../../../queries/project/project-delete-queries'; -import * as project_queries from '../../../queries/project/project-view-queries'; -import * as survey_view_queries from '../../../queries/survey/survey-view-queries'; +import { HTTPError } from '../../../errors/custom-error'; +import project_queries from '../../../queries/project'; +import survey_queries from '../../../queries/survey'; +import * as file_utils from '../../../utils/file-utils'; import { getMockDBConnection } from '../../../__mocks__/db'; import * as delete_project from './delete'; import * as survey_delete from './survey/{surveyId}/delete'; -import * as file_utils from '../../../utils/file-utils'; chai.use(sinonChai); @@ -59,8 +58,8 @@ describe('deleteProject', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: `projectId`'); } }); @@ -80,8 +79,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -106,8 +105,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get the project'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get the project'); } }); @@ -136,12 +135,12 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get the project'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get the project'); } }); - it('should throw a 400 error when user has insufficient role to delete', async () => { + it('should throw a 400 error when user has insufficient role to delete published project', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -164,14 +163,16 @@ describe('deleteProject', () => { const result = delete_project.deleteProject(); await result( - { ...sampleReq, system_user: { role_names: [SYSTEM_ROLE.PROJECT_ADMIN] } }, + { ...sampleReq, system_user: { role_names: [SYSTEM_ROLE.PROJECT_CREATOR] } }, (null as unknown) as any, (null as unknown) as any ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Cannot delete a published project if you are not a system administrator.'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Cannot delete a published project if you are not a system administrator.' + ); } }); @@ -193,8 +194,8 @@ describe('deleteProject', () => { } }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`some nice query`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(null); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`some nice query`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(null); try { const result = delete_project.deleteProject(); @@ -202,8 +203,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -231,8 +232,8 @@ describe('deleteProject', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(SQL`something`); try { const result = delete_project.deleteProject(); @@ -240,8 +241,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get project attachments'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get project attachments'); } }); @@ -272,8 +273,8 @@ describe('deleteProject', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(SQL`something`); try { const result = delete_project.deleteProject(); @@ -281,8 +282,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get survey ids associated to project'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get survey ids associated to project'); } }); @@ -313,10 +314,10 @@ describe('deleteProject', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(SQL`something`); sinon.stub(survey_delete, 'getSurveyAttachmentS3Keys').resolves(['key1', 'key2']); - sinon.stub(project_delete_queries, 'deleteProjectSQL').returns(null); + sinon.stub(project_queries, 'deleteProjectSQL').returns(null); try { const result = delete_project.deleteProject(); @@ -324,8 +325,8 @@ describe('deleteProject', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -359,10 +360,10 @@ describe('deleteProject', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(SQL`something`); sinon.stub(survey_delete, 'getSurveyAttachmentS3Keys').resolves(['key1', 'key2']); - sinon.stub(project_delete_queries, 'deleteProjectSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectSQL').returns(SQL`some`); sinon.stub(file_utils, 'deleteFileFromS3').resolves(null); const result = delete_project.deleteProject(); @@ -402,10 +403,10 @@ describe('deleteProject', () => { query: mockQuery }); - sinon.stub(project_attachments_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_view_queries, 'getSurveyIdsSQL').returns(SQL`something`); + sinon.stub(project_queries, 'getProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyIdsSQL').returns(SQL`something`); sinon.stub(survey_delete, 'getSurveyAttachmentS3Keys').resolves(['key1', 'key2']); - sinon.stub(project_delete_queries, 'deleteProjectSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectSQL').returns(SQL`some`); sinon.stub(file_utils, 'deleteFileFromS3').resolves({}); const result = delete_project.deleteProject(); diff --git a/api/src/paths/project/{projectId}/delete.ts b/api/src/paths/project/{projectId}/delete.ts index e9a9d3717e..ccef35a2f8 100644 --- a/api/src/paths/project/{projectId}/delete.ts +++ b/api/src/paths/project/{projectId}/delete.ts @@ -1,27 +1,35 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; +import { PROJECT_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400 } from '../../../errors/CustomError'; -import { getProjectAttachmentsSQL } from '../../../queries/project/project-attachments-queries'; +import { HTTP400 } from '../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../services/project-service'; import { getLogger } from '../../../utils/logger'; -import { getSurveyIdsSQL } from '../../../queries/survey/survey-view-queries'; -import { getSurveyAttachmentS3Keys } from './survey/{surveyId}/delete'; -import { deleteProjectSQL } from '../../../queries/project/project-delete-queries'; -import { deleteFileFromS3 } from '../../../utils/file-utils'; -import { getProjectSQL } from '../../../queries/project/project-view-queries'; -import { userHasValidSystemRoles } from '../../../security/auth-utils'; const defaultLog = getLogger('/api/project/{projectId}/delete'); -export const DELETE: Operation = [deleteProject()]; +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteProject() +]; DELETE.apiDoc = { description: 'Delete a project.', tags: ['project'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -64,104 +72,19 @@ export function deleteProject(): RequestHandler { } const connection = getDBConnection(req['keycloak_token']); + const projectId = Number(req.params.projectId); + const userRoles = req['system_user']['role_names']; try { - /** - * PART 1 - * Check that user is a system administrator - can delete a project (published or not) - * Check that user is a project administrator - can delete a project (unpublished only) - * - */ - - const getProjectSQLStatement = getProjectSQL(Number(req.params.projectId)); - - if (!getProjectSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const projectData = await connection.query(getProjectSQLStatement.text, getProjectSQLStatement.values); - - const projectResult = (projectData && projectData.rows && projectData.rows[0]) || null; - - if (!projectResult || !projectResult.id) { - throw new HTTP400('Failed to get the project'); - } - - if ( - projectResult.publish_date && - userHasValidSystemRoles([SYSTEM_ROLE.PROJECT_ADMIN], req['system_user']['role_names']) - ) { - throw new HTTP400('Cannot delete a published project if you are not a system administrator.'); - } - - /** - * PART 2 - * Get the attachment S3 keys for all attachments associated to this project and surveys under this project - * Used to delete them from S3 separately later - */ - const getProjectAttachmentSQLStatement = getProjectAttachmentsSQL(Number(req.params.projectId)); - const getSurveyIdsSQLStatement = getSurveyIdsSQL(Number(req.params.projectId)); - - if (!getProjectAttachmentSQLStatement || !getSurveyIdsSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const getProjectAttachmentsResult = await connection.query( - getProjectAttachmentSQLStatement.text, - getProjectAttachmentSQLStatement.values - ); + const projectService = new ProjectService(connection); - if (!getProjectAttachmentsResult || !getProjectAttachmentsResult.rows) { - throw new HTTP400('Failed to get project attachments'); - } - - const getSurveyIdsResult = await connection.query(getSurveyIdsSQLStatement.text, getSurveyIdsSQLStatement.values); - - if (!getSurveyIdsResult || !getSurveyIdsResult.rows) { - throw new HTTP400('Failed to get survey ids associated to project'); - } - - const surveyAttachmentS3Keys: string[] = Array.prototype.concat.apply( - [], - await Promise.all( - getSurveyIdsResult.rows.map((survey: any) => getSurveyAttachmentS3Keys(survey.id, connection)) - ) - ); - - const projectAttachmentS3Keys: string[] = getProjectAttachmentsResult.rows.map((attachment: any) => { - return attachment.key; - }); - - /** - * PART 3 - * Delete the project and all associated records/resources from our DB - */ - const deleteProjectSQLStatement = deleteProjectSQL(Number(req.params.projectId)); - - if (!deleteProjectSQLStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - await connection.query(deleteProjectSQLStatement.text, deleteProjectSQLStatement.values); - - /** - * PART 3 - * Delete the project and survey attachments from S3 - */ - const deleteResult = [ - ...(await Promise.all(projectAttachmentS3Keys.map((projectS3Key: string) => deleteFileFromS3(projectS3Key)))), - ...(await Promise.all(surveyAttachmentS3Keys.map((surveyS3Key: string) => deleteFileFromS3(surveyS3Key)))) - ]; - - if (deleteResult.some((deleteResult) => !deleteResult)) { - return res.status(200).json(null); - } + const resp = await projectService.deleteProject(projectId, userRoles); await connection.commit(); - return res.status(200).json(true); + return res.status(200).json(resp); } catch (error) { defaultLog.error({ label: 'deleteProject', message: 'error', error }); await connection.rollback(); diff --git a/api/src/paths/project/{projectId}/funding-sources/add.test.ts b/api/src/paths/project/{projectId}/funding-sources/add.test.ts index 8a6c8ae971..b804a31679 100644 --- a/api/src/paths/project/{projectId}/funding-sources/add.test.ts +++ b/api/src/paths/project/{projectId}/funding-sources/add.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as addFunding from './add'; -import * as db from '../../../../database/db'; -import * as addFundingSource_queries from '../../../../queries/project/project-create-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import project_queries from '../../../../queries/project'; import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as addFunding from './add'; chai.use(sinonChai); @@ -57,8 +58,8 @@ describe('add a funding source', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -76,8 +77,8 @@ describe('add a funding source', () => { await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing funding source data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing funding source data'); } }); @@ -94,7 +95,7 @@ describe('add a funding source', () => { query: mockQuery }); - sinon.stub(addFundingSource_queries, 'postProjectFundingSourceSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'postProjectFundingSourceSQL').returns(SQL`some query`); try { const result = addFunding.addFundingSource(); @@ -102,8 +103,8 @@ describe('add a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert project funding source data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert project funding source data'); } }); @@ -115,7 +116,7 @@ describe('add a funding source', () => { } }); - sinon.stub(addFundingSource_queries, 'postProjectFundingSourceSQL').returns(null); + sinon.stub(project_queries, 'postProjectFundingSourceSQL').returns(null); try { const result = addFunding.addFundingSource(); @@ -123,8 +124,8 @@ describe('add a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build addFundingSourceSQLStatement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build addFundingSourceSQLStatement'); } }); @@ -141,7 +142,7 @@ describe('add a funding source', () => { query: mockQuery }); - sinon.stub(addFundingSource_queries, 'postProjectFundingSourceSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'postProjectFundingSourceSQL').returns(SQL`some query`); try { const result = addFunding.addFundingSource(); @@ -149,8 +150,8 @@ describe('add a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert project funding source data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert project funding source data'); } }); @@ -167,7 +168,7 @@ describe('add a funding source', () => { query: mockQuery }); - sinon.stub(addFundingSource_queries, 'postProjectFundingSourceSQL').returns(SQL`something`); + sinon.stub(project_queries, 'postProjectFundingSourceSQL').returns(SQL`something`); const result = addFunding.addFundingSource(); diff --git a/api/src/paths/project/{projectId}/funding-sources/add.ts b/api/src/paths/project/{projectId}/funding-sources/add.ts index 564e3571ed..b161c173f4 100644 --- a/api/src/paths/project/{projectId}/funding-sources/add.ts +++ b/api/src/paths/project/{projectId}/funding-sources/add.ts @@ -1,17 +1,30 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../constants/roles'; import { getDBConnection } from '../../../../database/db'; -import { HTTP400 } from '../../../../errors/CustomError'; +import { HTTP400 } from '../../../../errors/custom-error'; import { PostFundingSource } from '../../../../models/project-create'; -import { postProjectFundingSourceSQL } from '../../../../queries/project/project-create-queries'; +import { queries } from '../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../utils/logger'; import { addFundingSourceApiDocObject } from '../../../../utils/shared-api-docs'; const defaultLog = getLogger('/api/projects/{projectId}/funding-sources/add'); -export const POST: Operation = [addFundingSource()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + addFundingSource() +]; POST.apiDoc = addFundingSourceApiDocObject('Add a funding source of a project.', 'new project funding source id'); @@ -39,7 +52,7 @@ export function addFundingSource(): RequestHandler { try { await connection.open(); - const addFundingSourceSQLStatement = postProjectFundingSourceSQL( + const addFundingSourceSQLStatement = queries.project.postProjectFundingSourceSQL( sanitizedPostFundingSource, Number(req.params.projectId) ); diff --git a/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.test.ts b/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.test.ts index 6e8d037f4c..27a5d92a72 100644 --- a/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.test.ts @@ -2,12 +2,13 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as deleteFundingSource from './delete'; -import * as db from '../../../../../database/db'; -import * as project_delete_queries from '../../../../../queries/project/project-delete-queries'; -import * as survey_delete_queries from '../../../../../queries/survey/survey-delete-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import project_queries from '../../../../../queries/project'; +import survey_queries from '../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as deleteFundingSource from './delete'; chai.use(sinonChai); @@ -51,8 +52,8 @@ describe('delete a funding source', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -68,8 +69,8 @@ describe('delete a funding source', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `pfsId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `pfsId`'); } }); @@ -81,8 +82,8 @@ describe('delete a funding source', () => { } }); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(null); - sinon.stub(project_delete_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(null); + sinon.stub(project_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some`); try { const result = deleteFundingSource.deleteFundingSource(); @@ -90,8 +91,8 @@ describe('delete a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -103,8 +104,8 @@ describe('delete a funding source', () => { } }); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); - sinon.stub(project_delete_queries, 'deleteProjectFundingSourceSQL').returns(null); + sinon.stub(survey_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectFundingSourceSQL').returns(null); try { const result = deleteFundingSource.deleteFundingSource(); @@ -112,8 +113,8 @@ describe('delete a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -130,8 +131,8 @@ describe('delete a funding source', () => { query: mockQuery }); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); - sinon.stub(project_delete_queries, 'deleteProjectFundingSourceSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectFundingSourceSQL').returns(SQL`something`); const result = deleteFundingSource.deleteFundingSource(); @@ -153,8 +154,8 @@ describe('delete a funding source', () => { query: mockQuery }); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); - sinon.stub(project_delete_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some query`); try { const result = deleteFundingSource.deleteFundingSource(); @@ -162,8 +163,8 @@ describe('delete a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to delete survey funding source'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to delete project funding source'); } }); @@ -180,8 +181,8 @@ describe('delete a funding source', () => { query: mockQuery }); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); - sinon.stub(project_delete_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL').returns(SQL`some`); + sinon.stub(project_queries, 'deleteProjectFundingSourceSQL').returns(SQL`some query`); try { const result = deleteFundingSource.deleteFundingSource(); @@ -189,8 +190,8 @@ describe('delete a funding source', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to delete project funding source'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to delete project funding source'); } }); }); diff --git a/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.ts b/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.ts index 9edcc1a907..f5494918dd 100644 --- a/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.ts +++ b/api/src/paths/project/{projectId}/funding-sources/{pfsId}/delete.ts @@ -1,17 +1,29 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { deleteProjectFundingSourceSQL } from '../../../../../queries/project/project-delete-queries'; -import { deleteSurveyFundingSourceByProjectFundingSourceIdSQL } from '../../../../../queries/survey/survey-delete-queries'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; import { deleteFundingSourceApiDocObject } from '../../../../../utils/shared-api-docs'; const defaultLog = getLogger('/api/projects/{projectId}/funding-sources/{pfsId}/delete'); -export const DELETE: Operation = [deleteFundingSource()]; +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.query.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteFundingSource() +]; DELETE.apiDoc = deleteFundingSourceApiDocObject( 'Delete a funding source of a project.', @@ -35,10 +47,11 @@ export function deleteFundingSource(): RequestHandler { try { await connection.open(); - const surveyFundingSourceDeleteStatement = deleteSurveyFundingSourceByProjectFundingSourceIdSQL( + const surveyFundingSourceDeleteStatement = queries.survey.deleteSurveyFundingSourceByProjectFundingSourceIdSQL( Number(req.params.pfsId) ); - const deleteProjectFundingSourceSQLStatement = deleteProjectFundingSourceSQL( + + const deleteProjectFundingSourceSQLStatement = queries.project.deleteProjectFundingSourceSQL( Number(req.params.projectId), Number(req.params.pfsId) ); @@ -47,21 +60,14 @@ export function deleteFundingSource(): RequestHandler { throw new HTTP400('Failed to build SQL delete statement'); } - const surveyFundingSourceDeleteResponse = await connection.query( - surveyFundingSourceDeleteStatement.text, - surveyFundingSourceDeleteStatement.values - ); - - if (!surveyFundingSourceDeleteResponse || !surveyFundingSourceDeleteResponse.rowCount) { - throw new HTTP400('Failed to delete survey funding source'); - } + await connection.query(surveyFundingSourceDeleteStatement.text, surveyFundingSourceDeleteStatement.values); const projectFundingSourceDeleteResponse = await connection.query( deleteProjectFundingSourceSQLStatement.text, deleteProjectFundingSourceSQLStatement.values ); - if (!projectFundingSourceDeleteResponse || !projectFundingSourceDeleteResponse.rowCount) { + if (!projectFundingSourceDeleteResponse.rowCount) { throw new HTTP400('Failed to delete project funding source'); } diff --git a/api/src/paths/project/{projectId}/participants/create.test.ts b/api/src/paths/project/{projectId}/participants/create.test.ts new file mode 100644 index 0000000000..350d864821 --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/create.test.ts @@ -0,0 +1,83 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { SYSTEM_IDENTITY_SOURCE } from '../../../../constants/database'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { UserService } from '../../../../services/user-service'; +import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as create_project_participants from './create'; + +chai.use(sinonChai); + +describe('createProjectParticipants', () => { + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + body: { + participants: [['jsmith', SYSTEM_IDENTITY_SOURCE.IDIR, 1]] + }, + params: { + projectId: 1 + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId in the param', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = create_project_participants.createProjectParticipants(); + await result( + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param `projectId`'); + } + }); + + it('should throw a 400 error when no participants in the request body', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = create_project_participants.createProjectParticipants(); + await result( + { ...sampleReq, body: { ...sampleReq.body, participants: [] } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `participants`'); + } + }); + + it('should catch and re-throw an error thrown by ensureSystemUserAndProjectParticipantUser', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: null + }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + sinon.stub(UserService.prototype, 'ensureSystemUser').rejects(new Error('an error')); + + try { + const result = create_project_participants.createProjectParticipants(); + await result({ ...sampleReq }, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('an error'); + } + }); +}); diff --git a/api/src/paths/project/{projectId}/participants/create.ts b/api/src/paths/project/{projectId}/participants/create.ts new file mode 100644 index 0000000000..b06dd1d135 --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/create.ts @@ -0,0 +1,155 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_IDENTITY_SOURCE } from '../../../../constants/database'; +import { PROJECT_ROLE } from '../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../../services/project-service'; +import { UserService } from '../../../../services/user-service'; +import { getLogger } from '../../../../utils/logger'; + +const defaultLog = getLogger('paths/project/{projectId}/participants/create'); + +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + createProjectParticipants() +]; + +POST.apiDoc = { + description: 'Get all project participants.', + tags: ['project'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + required: ['participants'], + properties: { + participants: { + type: 'array', + items: { + type: 'object', + required: ['userIdentifier', 'identitySource', 'roleId'], + properties: { + userIdentifier: { + description: 'A IDIR or BCEID username.', + type: 'string' + }, + identitySource: { + type: 'string', + enum: [SYSTEM_IDENTITY_SOURCE.IDIR, SYSTEM_IDENTITY_SOURCE.BCEID] + }, + roleId: { + description: 'The id of the project role to assign to the participant.', + type: 'number' + } + } + } + } + } + } + } + } + }, + responses: { + 200: { + description: 'Project participants added OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function createProjectParticipants(): RequestHandler { + return async (req, res) => { + if (!req.params.projectId) { + throw new HTTP400('Missing required param `projectId`'); + } + + if (!req.body.participants || !req.body.participants.length) { + throw new HTTP400('Missing required body param `participants`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const projectId = Number(req.params.projectId); + + const participants: { userIdentifier: string; identitySource: string; roleId: number }[] = req.body.participants; + + await connection.open(); + + const promises: Promise[] = []; + + participants.forEach((participant) => + promises.push(ensureSystemUserAndProjectParticipantUser(projectId, participant, connection)) + ); + + await Promise.all(promises); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'insertProjectParticipants', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} + +export const ensureSystemUserAndProjectParticipantUser = async ( + projectId: number, + participant: { userIdentifier: string; identitySource: string; roleId: number }, + connection: IDBConnection +) => { + const userService = new UserService(connection); + + // Add a system user, unless they already have one + const systemUserObject = await userService.ensureSystemUser(participant.userIdentifier, participant.identitySource); + + const projectService = new ProjectService(connection); + + // Add project role, unless they already have one + await projectService.ensureProjectParticipant(projectId, systemUserObject.id, participant.roleId); +}; diff --git a/api/src/paths/project/{projectId}/participants/get.test.ts b/api/src/paths/project/{projectId}/participants/get.test.ts new file mode 100644 index 0000000000..bfd1fdfdaa --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/get.test.ts @@ -0,0 +1,77 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { ProjectService } from '../../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import * as get_project_participants from './get'; + +chai.use(sinonChai); + +describe('gets a list of project participants', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = get_project_participants.getParticipants(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param `projectId`'); + } + }); + + it('should catch and re-throw an error if ProjectService throws an error', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1' + }; + + sinon.stub(ProjectService.prototype, 'getProjectParticipants').rejects(new Error('an error')); + + try { + const requestHandler = get_project_participants.getParticipants(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('an error'); + } + }); + + it('should return participants on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1' + }; + + sinon.stub(ProjectService.prototype, 'getProjectParticipants').resolves([{ id: 1 }]); + + const requestHandler = get_project_participants.getParticipants(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.jsonValue).to.eql({ participants: [{ id: 1 }] }); + }); +}); diff --git a/api/src/paths/project/{projectId}/participants/get.ts b/api/src/paths/project/{projectId}/participants/get.ts new file mode 100644 index 0000000000..17045c7e9a --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/get.ts @@ -0,0 +1,137 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../../services/project-service'; +import { getLogger } from '../../../../utils/logger'; + +const defaultLog = getLogger('paths/project/{projectId}/participants/get'); + +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getParticipants() +]; + +GET.apiDoc = { + description: 'Get all project participants.', + tags: ['project'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'List of project participants.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + participants: { + type: 'array', + items: { + type: 'object', + properties: { + project_participation_id: { + type: 'number' + }, + project_id: { + type: 'number' + }, + system_user_id: { + type: 'number' + }, + project_role_id: { + type: 'number' + }, + project_role_name: { + type: 'string' + }, + user_identifier: { + type: 'string' + }, + user_identity_source_id: { + type: 'number' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get all project participants. + * + * @returns {RequestHandler} + */ +export function getParticipants(): RequestHandler { + return async (req, res) => { + if (!req.params.projectId) { + throw new HTTP400('Missing required param `projectId`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const projectId = Number(req.params.projectId); + + await connection.open(); + + const projectService = new ProjectService(connection); + + const result = await projectService.getProjectParticipants(projectId); + + await connection.commit(); + + return res.status(200).json({ participants: result }); + } catch (error) { + defaultLog.error({ label: 'getAllProjectParticipantsSQL', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.test.ts b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.test.ts new file mode 100644 index 0000000000..585b609870 --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.test.ts @@ -0,0 +1,194 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { ProjectService } from '../../../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../__mocks__/db'; +import * as doAllProjectsHaveAProjectLead from '../../../../user/{userId}/delete'; +import * as delete_project_participant from './delete'; +chai.use(sinonChai); + +describe('Delete a project participant.', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + mockReq.params = { projectId: '', projectParticipationId: '2' }; + + try { + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no projectParticipationId is provided', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + mockReq.params = { projectId: '1', projectParticipationId: '' }; + + try { + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectParticipationId`'); + } + }); + + it('should throw a 400 error when deleteProjectParticipationSQL query fails', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(null); + sinon.stub(ProjectService.prototype, 'getProjectParticipants').resolves([{ id: 1 }]); + sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead').returns(true); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); + } + }); + + it('should throw a 400 error when connection query fails', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + sinon.stub(ProjectService.prototype, 'getProjectParticipants').resolves([{ id: 1 }]); + sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead').returns(true); + + const mockQuery = sinon.stub(); + + mockQuery.resolves(null); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to delete project team member'); + } + }); + + it('should throw a 400 error when user is only project lead', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + const getProjectParticipant = sinon.stub(ProjectService.prototype, 'getProjectParticipants'); + const doAllProjectsHaveLead = sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead'); + + getProjectParticipant.onCall(0).resolves([{ id: 1 }]); + doAllProjectsHaveLead.onCall(0).returns(true); + getProjectParticipant.onCall(1).resolves([{ id: 2 }]); + doAllProjectsHaveLead.onCall(1).returns(false); + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [{ system_user_id: 1 }], + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + try { + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Cannot delete project user. User is the only Project Lead for the project.' + ); + } + }); + + it('should not throw an error', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + const getProjectParticipant = sinon.stub(ProjectService.prototype, 'getProjectParticipants'); + const doAllProjectsHaveLead = sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead'); + + getProjectParticipant.onCall(0).resolves([{ id: 1 }]); + doAllProjectsHaveLead.onCall(0).returns(true); + getProjectParticipant.onCall(1).resolves([{ id: 2 }]); + doAllProjectsHaveLead.onCall(1).returns(true); + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [{ system_user_id: 1 }], + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + const requestHandler = delete_project_participant.deleteProjectParticipant(); + + await requestHandler(mockReq, mockRes, mockNext); + expect(mockRes.statusValue).to.equal(200); + }); +}); diff --git a/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.ts b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.ts new file mode 100644 index 0000000000..9342241ba8 --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/delete.ts @@ -0,0 +1,148 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../database/db'; +import { HTTP400, HTTP500 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../../../services/project-service'; +import { getLogger } from '../../../../../utils/logger'; +import { doAllProjectsHaveAProjectLead } from '../../../../user/{userId}/delete'; + +const defaultLog = getLogger('/api/project/{projectId}/participants/{projectParticipationId}/delete'); + +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteProjectParticipant() +]; + +DELETE.apiDoc = { + description: 'Delete a project participant.', + tags: ['project'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'projectParticipationId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Delete project participant OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function deleteProjectParticipant(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'deleteProjectParticipant', message: 'params', req_params: req.params }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.projectParticipationId) { + throw new HTTP400('Missing required path param `projectParticipationId`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const projectService = new ProjectService(connection); + + // Check project lead roles before deleting user + const projectParticipantsResponse1 = await projectService.getProjectParticipants(Number(req.params.projectId)); + const projectHasLeadResponse1 = doAllProjectsHaveAProjectLead(projectParticipantsResponse1); + + const result = await deleteProjectParticipationRecord(Number(req.params.projectParticipationId), connection); + + if (!result || !result.system_user_id) { + // The delete result is missing necesary data, fail the request + throw new HTTP500('Failed to delete project participant'); + } + + // If Project Lead roles are invalide skip check to prevent removal of only Project Lead of project + // (Project is already missing Project Lead and is in a bad state) + if (projectHasLeadResponse1) { + const projectParticipantsResponse2 = await projectService.getProjectParticipants(Number(req.params.projectId)); + const projectHasLeadResponse2 = doAllProjectsHaveAProjectLead(projectParticipantsResponse2); + + if (!projectHasLeadResponse2) { + throw new HTTP400('Cannot delete project user. User is the only Project Lead for the project.'); + } + } + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'deleteProjectParticipant', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const deleteProjectParticipationRecord = async ( + projectParticipationId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.projectParticipation.deleteProjectParticipationSQL(projectParticipationId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP500('Failed to delete project team member'); + } + + return response.rows[0]; +}; diff --git a/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.test.ts b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.test.ts new file mode 100644 index 0000000000..524715b1b9 --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.test.ts @@ -0,0 +1,216 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { ProjectService } from '../../../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../__mocks__/db'; +import * as doAllProjectsHaveAProjectLead from '../../../../user/{userId}/delete'; +import * as update_project_participant from './update'; +chai.use(sinonChai); + +describe('Delete a project participant.', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + mockReq.params = { projectId: '', projectParticipationId: '2' }; + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no projectParticipationId is provided', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + mockReq.params = { projectId: '1', projectParticipationId: '' }; + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectParticipationId`'); + } + }); + + it('should throw a 400 error when no roleId is provided', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + mockReq.body = { roleId: '' }; + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `roleId`'); + } + }); + + it('should throw a 400 error when deleteProjectParticipationSQL query fails', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + mockReq.body = { roleId: '1' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(null); + sinon.stub(ProjectService.prototype, 'getProjectParticipants').resolves([{ id: 1 }]); + sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead').returns(true); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); + } + }); + + it('should throw a 400 error when connection query fails', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + mockReq.body = { roleId: '1' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + sinon.stub(ProjectService.prototype, 'getProjectParticipants').resolves([{ id: 1 }]); + sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead').returns(true); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to delete project team member'); + } + }); + + it('should throw a 400 error when user is only project lead', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + mockReq.body = { roleId: '1' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + const getProjectParticipant = sinon.stub(ProjectService.prototype, 'getProjectParticipants'); + const doAllProjectsHaveLead = sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead'); + + getProjectParticipant.onCall(0).resolves([{ id: 1 }]); + doAllProjectsHaveLead.onCall(0).returns(true); + getProjectParticipant.onCall(1).resolves([{ id: 2 }]); + doAllProjectsHaveLead.onCall(1).returns(false); + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [{ system_user_id: 1 }], + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + try { + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Cannot update project user. User is the only Project Lead for the project.' + ); + } + }); + + it('should not throw an error', async () => { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const dbConnectionObj = getMockDBConnection(); + + mockReq.params = { projectId: '1', projectParticipationId: '2' }; + mockReq.body = { roleId: '1' }; + + sinon.stub(queries.projectParticipation, 'deleteProjectParticipationSQL').returns(SQL`some query`); + const getProjectParticipant = sinon.stub(ProjectService.prototype, 'getProjectParticipants'); + const doAllProjectsHaveLead = sinon.stub(doAllProjectsHaveAProjectLead, 'doAllProjectsHaveAProjectLead'); + + getProjectParticipant.onCall(0).resolves([{ id: 1 }]); + doAllProjectsHaveLead.onCall(0).returns(true); + getProjectParticipant.onCall(1).resolves([{ id: 2 }]); + doAllProjectsHaveLead.onCall(1).returns(true); + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [{ system_user_id: 1 }], + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + const requestHandler = update_project_participant.updateProjectParticipantRole(); + + await requestHandler(mockReq, mockRes, mockNext); + expect(mockRes.statusValue).to.equal(200); + }); +}); diff --git a/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.ts b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.ts new file mode 100644 index 0000000000..115249a4df --- /dev/null +++ b/api/src/paths/project/{projectId}/participants/{projectParticipationId}/update.ts @@ -0,0 +1,155 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; +import { getDBConnection } from '../../../../../database/db'; +import { HTTP400, HTTP500 } from '../../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../../../services/project-service'; +import { getLogger } from '../../../../../utils/logger'; +import { doAllProjectsHaveAProjectLead } from '../../../../user/{userId}/delete'; +import { deleteProjectParticipationRecord } from './delete'; + +const defaultLog = getLogger('/api/project/{projectId}/participants/{projectParticipationId}/update'); + +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateProjectParticipantRole() +]; + +PUT.apiDoc = { + description: 'Update a project participant role.', + tags: ['project'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'projectParticipationId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + required: ['roleId'], + properties: { + roleId: { + type: 'number' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Update project participant role OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function updateProjectParticipantRole(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'updateProjectParticipantRole', message: 'params', req_params: req.params }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.projectParticipationId) { + throw new HTTP400('Missing required path param `projectParticipationId`'); + } + + if (!req.body.roleId) { + throw new HTTP400('Missing required body param `roleId`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const projectService = new ProjectService(connection); + + // Check project lead roles before updating user + const projectParticipantsResponse1 = await projectService.getProjectParticipants(Number(req.params.projectId)); + const projectHasLeadResponse1 = doAllProjectsHaveAProjectLead(projectParticipantsResponse1); + + // Delete the user's old participation record, returning the old record + const result = await deleteProjectParticipationRecord(Number(req.params.projectParticipationId), connection); + + if (!result || !result.system_user_id) { + // The delete result is missing necessary data, fail the request + throw new HTTP500('Failed to update project participant role'); + } + + await projectService.addProjectParticipant( + Number(req.params.projectId), + Number(result.system_user_id), // get the user's system id from the old participation record + Number(req.body.roleId) + ); + + // If Project Lead roles are invalid skip check to prevent removal of only Project Lead of project + // (Project is already missing Project Lead and is in a bad state) + if (projectHasLeadResponse1) { + const projectParticipantsResponse2 = await projectService.getProjectParticipants(Number(req.params.projectId)); + const projectHasLeadResponse2 = doAllProjectsHaveAProjectLead(projectParticipantsResponse2); + + if (!projectHasLeadResponse2) { + throw new HTTP400('Cannot update project user. User is the only Project Lead for the project.'); + } + } + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateProjectParticipantRole', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/{projectId}/publish.test.ts b/api/src/paths/project/{projectId}/publish.test.ts index f7ebeb12e4..8cbdd3dcb7 100644 --- a/api/src/paths/project/{projectId}/publish.test.ts +++ b/api/src/paths/project/{projectId}/publish.test.ts @@ -1,13 +1,13 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; +import { QueryResult } from 'pg'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as publish from './publish'; import * as db from '../../../database/db'; -import * as project_update_queries from '../../../queries/project/project-update-queries'; -import { QueryResult } from 'pg'; -import SQL from 'sql-template-strings'; +import { HTTPError } from '../../../errors/custom-error'; +import { ProjectService } from '../../../services/project-service'; import { getMockDBConnection } from '../../../__mocks__/db'; +import * as publish from './publish'; chai.use(sinonChai); @@ -60,8 +60,8 @@ describe('project/{projectId}/publish', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path parameter: projectId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: projectId'); } }); @@ -83,8 +83,8 @@ describe('project/{projectId}/publish', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing request body'); } }); @@ -106,54 +106,8 @@ describe('project/{projectId}/publish', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing publish flag in request body'); - } - }); - - it('should throw a 400 error when no sql statement produced', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - sinon.stub(project_update_queries, 'updateProjectPublishStatusSQL').returns(null); - - try { - const result = publish.publishProject(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement'); - } - }); - - it('should throw a 500 error when no result', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return { - rows: null - } as any; - } - }); - - sinon.stub(project_update_queries, 'updateProjectPublishStatusSQL').returns(SQL`some query`); - - try { - const result = publish.publishProject(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to update project publish status'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing publish flag in request body'); } }); @@ -176,7 +130,7 @@ describe('project/{projectId}/publish', () => { } }); - sinon.stub(project_update_queries, 'updateProjectPublishStatusSQL').returns(SQL`some query`); + sinon.stub(ProjectService.prototype, 'updatePublishStatus').resolves(1); const result = publish.publishProject(); diff --git a/api/src/paths/project/{projectId}/publish.ts b/api/src/paths/project/{projectId}/publish.ts index eed79930bb..1ae11be4e1 100644 --- a/api/src/paths/project/{projectId}/publish.ts +++ b/api/src/paths/project/{projectId}/publish.ts @@ -1,23 +1,36 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; +import { PROJECT_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400, HTTP500 } from '../../../errors/CustomError'; +import { HTTP400 } from '../../../errors/custom-error'; import { projectIdResponseObject } from '../../../openapi/schemas/project'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../services/project-service'; import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; -import { updateProjectPublishStatusSQL } from '../../../queries/project/project-update-queries'; const defaultLog = getLogger('paths/project/{projectId}/publish'); -export const PUT: Operation = [logRequest('paths/project/{projectId}/publish', 'PUT'), publishProject()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + publishProject() +]; PUT.apiDoc = { description: 'Publish or unpublish a project.', tags: ['project'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -104,24 +117,14 @@ export function publishProject(): RequestHandler { const publish: boolean = req.body.publish; - const sqlStatement = updateProjectPublishStatusSQL(projectId, publish); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL statement'); - } - await connection.open(); - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; + const projectService = new ProjectService(connection); - if (!response || !result) { - throw new HTTP500('Failed to update project publish status'); - } + const result = await projectService.updatePublishStatus(projectId, publish); await connection.commit(); - return res.status(200).json({ id: result.id }); + return res.status(200).json({ id: result }); } catch (error) { defaultLog.error({ label: 'publishProject', message: 'error', error }); await connection.rollback(); diff --git a/api/src/paths/project/{projectId}/survey/create.test.ts b/api/src/paths/project/{projectId}/survey/create.test.ts index 09d7c579db..1b8e3e992d 100644 --- a/api/src/paths/project/{projectId}/survey/create.test.ts +++ b/api/src/paths/project/{projectId}/survey/create.test.ts @@ -2,12 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as create from './create'; -import * as db from '../../../../database/db'; -import * as survey_create_queries from '../../../../queries/survey/survey-create-queries'; -import * as survey_update_queries from '../../../../queries/survey/survey-update-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import survey_queries from '../../../../queries/survey'; import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as create from './create'; chai.use(sinonChai); @@ -28,9 +28,9 @@ describe('createSurvey', () => { end_date: '2080-12-30', first_nations_id: 0, foippa_requirements_accepted: true, + sedis_procedures_accepted: true, proprietary_data_category: 1, proprietor_name: 'test name', - sedis_procedures_accepted: true, focal_species: [], ancillary_species: [], start_date: '1925-12-23', @@ -69,8 +69,8 @@ describe('createSurvey', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -88,8 +88,8 @@ describe('createSurvey', () => { await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing survey data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing survey data'); } }); @@ -101,7 +101,7 @@ describe('createSurvey', () => { } }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(null); + sinon.stub(survey_queries, 'postSurveySQL').returns(null); try { const result = create.createSurvey(); @@ -109,8 +109,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build survey SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build survey SQL insert statement'); } }); @@ -127,7 +127,7 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`some query`); try { const result = create.createSurvey(); @@ -135,8 +135,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey data'); } }); @@ -153,7 +153,7 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`some query`); try { const result = create.createSurvey(); @@ -161,8 +161,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey data'); } }); @@ -179,8 +179,8 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`something`); - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(null); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(null); try { const result = create.createSurvey(); @@ -188,8 +188,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); @@ -206,7 +206,7 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`something`); const result = create.createSurvey(); @@ -234,7 +234,7 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`something`); sinon.stub(create, 'insertSurveyPermit').resolves(); sinon.stub(create, 'insertSurveyFundingSource').resolves(); @@ -267,7 +267,7 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`something`); sinon.stub(create, 'insertFocalSpecies').resolves(1); sinon.stub(create, 'insertAncillarySpecies').resolves(1); @@ -309,8 +309,8 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`something`); - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); const result = create.createSurvey(); @@ -338,8 +338,8 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`some query`); - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); try { const result = create.createSurvey(); @@ -347,8 +347,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey proprietor data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey proprietor data'); } }); @@ -369,8 +369,8 @@ describe('createSurvey', () => { query: mockQuery }); - sinon.stub(survey_create_queries, 'postSurveySQL').returns(SQL`some query`); - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); + sinon.stub(survey_queries, 'postSurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(SQL`something else`); try { const result = create.createSurvey(); @@ -378,8 +378,8 @@ describe('createSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey proprietor data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey proprietor data'); } }); }); @@ -401,15 +401,15 @@ describe('insertFocalSpecies', () => { it('should throw an error when cannot generate post sql statement', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_create_queries, 'postFocalSpeciesSQL').returns(null); + sinon.stub(survey_queries, 'postFocalSpeciesSQL').returns(null); try { await create.insertFocalSpecies(focalSpeciesId, surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); @@ -418,15 +418,15 @@ describe('insertFocalSpecies', () => { mockQuery.resolves({ rows: [null] }); - sinon.stub(survey_create_queries, 'postFocalSpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postFocalSpeciesSQL').returns(SQL`some`); try { await create.insertFocalSpecies(focalSpeciesId, surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert focal species data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert focal species data'); } }); @@ -435,15 +435,15 @@ describe('insertFocalSpecies', () => { mockQuery.resolves({ rows: [{ id: null }] }); - sinon.stub(survey_create_queries, 'postFocalSpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postFocalSpeciesSQL').returns(SQL`some`); try { await create.insertFocalSpecies(focalSpeciesId, surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert focal species data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert focal species data'); } }); @@ -452,7 +452,7 @@ describe('insertFocalSpecies', () => { mockQuery.resolves({ rows: [{ id: 12 }] }); - sinon.stub(survey_create_queries, 'postFocalSpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postFocalSpeciesSQL').returns(SQL`some`); const res = await create.insertFocalSpecies(focalSpeciesId, surveyId, { ...dbConnectionObj, query: mockQuery }); @@ -477,15 +477,15 @@ describe('insertAncillarySpecies', () => { it('should throw an error when cannot generate post sql statement', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_create_queries, 'postAncillarySpeciesSQL').returns(null); + sinon.stub(survey_queries, 'postAncillarySpeciesSQL').returns(null); try { await create.insertAncillarySpecies(ancillarySpeciesId, surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); @@ -494,15 +494,15 @@ describe('insertAncillarySpecies', () => { mockQuery.resolves({ rows: [null] }); - sinon.stub(survey_create_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); try { await create.insertAncillarySpecies(ancillarySpeciesId, surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert ancillary species data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert ancillary species data'); } }); @@ -511,15 +511,15 @@ describe('insertAncillarySpecies', () => { mockQuery.resolves({ rows: [{ id: null }] }); - sinon.stub(survey_create_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); try { await create.insertAncillarySpecies(ancillarySpeciesId, surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert ancillary species data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert ancillary species data'); } }); @@ -528,7 +528,7 @@ describe('insertAncillarySpecies', () => { mockQuery.resolves({ rows: [{ id: 12 }] }); - sinon.stub(survey_create_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postAncillarySpeciesSQL').returns(SQL`some`); const res = await create.insertAncillarySpecies(ancillarySpeciesId, surveyId, { ...dbConnectionObj, @@ -557,30 +557,30 @@ describe('insertSurveyPermit', () => { it('should throw an error when cannot generate post sql statement', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_create_queries, 'postNewSurveyPermitSQL').returns(null); + sinon.stub(survey_queries, 'postNewSurveyPermitSQL').returns(null); try { await create.insertSurveyPermit(permitNumber, 'type', projectId, surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement for insertSurveyPermit'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement for insertSurveyPermit'); } }); it('should throw an error when cannot generate put sql statement', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_update_queries, 'putNewSurveyPermitNumberSQL').returns(null); + sinon.stub(survey_queries, 'putNewSurveyPermitNumberSQL').returns(null); try { await create.insertSurveyPermit(permitNumber, null, projectId, surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement for insertSurveyPermit'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement for insertSurveyPermit'); } }); @@ -589,7 +589,7 @@ describe('insertSurveyPermit', () => { mockQuery.resolves(null); - sinon.stub(survey_create_queries, 'postNewSurveyPermitSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postNewSurveyPermitSQL').returns(SQL`some`); try { await create.insertSurveyPermit(permitNumber, 'type', projectId, surveyId, { @@ -599,8 +599,8 @@ describe('insertSurveyPermit', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey permit number data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey permit number data'); } }); @@ -609,7 +609,7 @@ describe('insertSurveyPermit', () => { mockQuery.resolves(null); - sinon.stub(survey_update_queries, 'putNewSurveyPermitNumberSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'putNewSurveyPermitNumberSQL').returns(SQL`some`); try { await create.insertSurveyPermit(permitNumber, null, projectId, surveyId, { @@ -619,8 +619,8 @@ describe('insertSurveyPermit', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey permit number data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey permit number data'); } }); }); @@ -641,15 +641,17 @@ describe('insertSurveyFundingSource', () => { it('should throw an error when cannot generate sql statement', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_create_queries, 'insertSurveyFundingSourceSQL').returns(null); + sinon.stub(survey_queries, 'insertSurveyFundingSourceSQL').returns(null); try { await create.insertSurveyFundingSource(fundingSourceId, surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement for insertSurveyFundingSource'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build SQL statement for insertSurveyFundingSource' + ); } }); @@ -658,15 +660,15 @@ describe('insertSurveyFundingSource', () => { mockQuery.resolves(null); - sinon.stub(survey_create_queries, 'insertSurveyFundingSourceSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'insertSurveyFundingSourceSQL').returns(SQL`something`); try { await create.insertSurveyFundingSource(fundingSourceId, surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey funding source data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey funding source data'); } }); }); diff --git a/api/src/paths/project/{projectId}/survey/create.ts b/api/src/paths/project/{projectId}/survey/create.ts index 7ef0af3f2c..38ad68e901 100644 --- a/api/src/paths/project/{projectId}/survey/create.ts +++ b/api/src/paths/project/{projectId}/survey/create.ts @@ -1,32 +1,36 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../database/db'; -import { HTTP400 } from '../../../../errors/CustomError'; +import { HTTP400 } from '../../../../errors/custom-error'; import { PostSurveyObject, PostSurveyProprietorData } from '../../../../models/survey-create'; -import { surveyCreatePostRequestObject, surveyIdResponseObject } from '../../../../openapi/schemas/survey'; -import { - insertSurveyFundingSourceSQL, - postAncillarySpeciesSQL, - postFocalSpeciesSQL, - postNewSurveyPermitSQL, - postSurveyProprietorSQL, - postSurveySQL -} from '../../../../queries/survey/survey-create-queries'; -import { putNewSurveyPermitNumberSQL } from '../../../../queries/survey/survey-update-queries'; +import { queries } from '../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../utils/logger'; -import { logRequest } from '../../../../utils/path-utils'; const defaultLog = getLogger('paths/project/{projectId}/survey/create'); -export const POST: Operation = [logRequest('paths/project/{projectId}/survey/create', 'POST'), createSurvey()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + createSurvey() +]; POST.apiDoc = { description: 'Create a new Survey.', tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -34,7 +38,126 @@ POST.apiDoc = { content: { 'application/json': { schema: { - ...(surveyCreatePostRequestObject as object) + title: 'SurveyProject post request object', + type: 'object', + required: [ + 'survey_name', + 'start_date', + 'end_date', + 'focal_species', + 'ancillary_species', + 'intended_outcome_id', + 'additional_details', + 'field_method_id', + 'vantage_code_ids', + 'ecological_season_id', + 'biologist_first_name', + 'biologist_last_name', + 'survey_area_name', + 'survey_data_proprietary', + 'foippa_requirements_accepted', + 'sedis_procedures_accepted' + ], + properties: { + survey_name: { + type: 'string' + }, + start_date: { + type: 'string', + description: 'ISO 8601 date string' + }, + end_date: { + type: 'string', + description: 'ISO 8601 date string' + }, + focal_species: { + type: 'array', + items: { + type: 'number' + }, + description: 'Selected focal species ids' + }, + ancillary_species: { + type: 'array', + items: { + type: 'number' + }, + description: 'Selected ancillary species ids' + }, + intended_outcome_id: { + type: 'number' + }, + additional_details: { + type: 'string' + }, + field_method_id: { + type: 'number' + }, + vantage_code_ids: { + type: 'array', + items: { + type: 'number' + } + }, + ecological_season_id: { + type: 'number' + }, + surveyed_all_areas: { + type: 'string', + enum: ['true', 'false'] + }, + biologist_first_name: { + type: 'string' + }, + biologist_last_name: { + type: 'string' + }, + survey_area_name: { + type: 'string' + }, + survey_data_proprietary: { + type: 'string' + }, + proprietary_data_category: { + type: 'number' + }, + proprietor_name: { + type: 'string' + }, + category_rationale: { + type: 'string' + }, + first_nations_id: { + type: 'number' + }, + data_sharing_agreement_required: { + type: 'string' + }, + foippa_requirements_accepted: { + type: 'boolean', + enum: [true], + description: + 'Data meets or exceeds the Freedom of Information and Protection of Privacy Act (FOIPPA) Requirements' + }, + sedis_procedures_accepted: { + type: 'boolean', + enum: [true], + description: + 'Data is in accordance with the Species and Ecosystems Data and Information Security (SEDIS) Procedures' + }, + funding_sources: { + type: 'array', + items: { + type: 'number' + } + }, + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + } + } } } } @@ -45,7 +168,14 @@ POST.apiDoc = { content: { 'application/json': { schema: { - ...(surveyIdResponseObject as object) + title: 'Survey Response Object', + type: 'object', + required: ['id'], + properties: { + id: { + type: 'number' + } + } } } } @@ -95,7 +225,7 @@ export function createSurvey(): RequestHandler { } try { - const postSurveySQLStatement = postSurveySQL(projectId, sanitizedPostSurveyData); + const postSurveySQLStatement = queries.survey.postSurveySQL(projectId, sanitizedPostSurveyData); if (!postSurveySQLStatement) { throw new HTTP400('Failed to build survey SQL insert statement'); @@ -164,6 +294,15 @@ export function createSurvey(): RequestHandler { sanitizedPostSurveyData.survey_proprietor && promises.push(insertSurveyProprietor(sanitizedPostSurveyData.survey_proprietor, surveyId, connection)); + //Handle vantage codes associated to this survey + promises.push( + Promise.all( + sanitizedPostSurveyData.vantage_code_ids.map((vantageCode: number) => + insertVantageCodes(vantageCode, surveyId, connection) + ) + ) + ); + await Promise.all(promises); await connection.commit(); @@ -187,7 +326,7 @@ export const insertFocalSpecies = async ( survey_id: number, connection: IDBConnection ): Promise => { - const sqlStatement = postFocalSpeciesSQL(focal_species_id, survey_id); + const sqlStatement = queries.survey.postFocalSpeciesSQL(focal_species_id, survey_id); if (!sqlStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -208,7 +347,28 @@ export const insertAncillarySpecies = async ( survey_id: number, connection: IDBConnection ): Promise => { - const sqlStatement = postAncillarySpeciesSQL(ancillary_species_id, survey_id); + const sqlStatement = queries.survey.postAncillarySpeciesSQL(ancillary_species_id, survey_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert ancillary species data'); + } + + return result.id; +}; + +export const insertVantageCodes = async ( + vantage_code_id: number, + survey_id: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.postVantageCodesSQL(vantage_code_id, survey_id); if (!sqlStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -229,7 +389,7 @@ export const insertSurveyProprietor = async ( survey_id: number, connection: IDBConnection ): Promise => { - const sqlStatement = postSurveyProprietorSQL(survey_id, survey_proprietor); + const sqlStatement = queries.survey.postSurveyProprietorSQL(survey_id, survey_proprietor); if (!sqlStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -255,11 +415,11 @@ export const insertSurveyPermit = async ( let sqlStatement; if (!permitType) { - sqlStatement = putNewSurveyPermitNumberSQL(surveyId, permitNumber); + sqlStatement = queries.survey.putNewSurveyPermitNumberSQL(surveyId, permitNumber); } else { const systemUserId = connection.systemUserId(); - sqlStatement = postNewSurveyPermitSQL(systemUserId, projectId, surveyId, permitNumber, permitType); + sqlStatement = queries.survey.postNewSurveyPermitSQL(systemUserId, projectId, surveyId, permitNumber, permitType); } if (!sqlStatement) { @@ -278,7 +438,7 @@ export const insertSurveyFundingSource = async ( survey_id: number, connection: IDBConnection ): Promise => { - const sqlStatement = insertSurveyFundingSourceSQL(survey_id, funding_source_id); + const sqlStatement = queries.survey.insertSurveyFundingSourceSQL(survey_id, funding_source_id); if (!sqlStatement) { throw new HTTP400('Failed to build SQL statement for insertSurveyFundingSource'); diff --git a/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts b/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts index 9649071179..5caff27bbb 100644 --- a/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts +++ b/api/src/paths/project/{projectId}/survey/funding-sources/list.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as list from './list'; -import * as db from '../../../../../database/db'; -import * as project_view_update_queries from '../../../../../queries/project/project-view-update-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import project_queries from '../../../../../queries/project'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as list from './list'; chai.use(sinonChai); @@ -54,8 +55,8 @@ describe('getSurveyFundingSources', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -67,7 +68,7 @@ describe('getSurveyFundingSources', () => { } }); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(null); + sinon.stub(project_queries, 'getFundingSourceByProjectSQL').returns(null); try { const result = list.getSurveyFundingSources(); @@ -75,8 +76,8 @@ describe('getSurveyFundingSources', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -108,7 +109,7 @@ describe('getSurveyFundingSources', () => { query: mockQuery }); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'getFundingSourceByProjectSQL').returns(SQL`some query`); const result = list.getSurveyFundingSources(); @@ -138,7 +139,7 @@ describe('getSurveyFundingSources', () => { query: mockQuery }); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some query`); + sinon.stub(project_queries, 'getFundingSourceByProjectSQL').returns(SQL`some query`); const result = list.getSurveyFundingSources(); diff --git a/api/src/paths/project/{projectId}/survey/funding-sources/list.ts b/api/src/paths/project/{projectId}/survey/funding-sources/list.ts index ebdf3feadc..1198798281 100644 --- a/api/src/paths/project/{projectId}/survey/funding-sources/list.ts +++ b/api/src/paths/project/{projectId}/survey/funding-sources/list.ts @@ -1,24 +1,36 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../errors/custom-error'; import { GetSurveyFundingSources } from '../../../../../models/survey-view'; -import { getFundingSourceByProjectSQL } from '../../../../../queries/project/project-view-update-queries'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/funding-sources/list'); -export const GET: Operation = [getSurveyFundingSources()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyFundingSources() +]; GET.apiDoc = { description: 'Fetches a list of funding sources for a survey based on a project.', tags: ['funding_sources'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -84,7 +96,9 @@ export function getSurveyFundingSources(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveyFundingSourcesSQLStatement = getFundingSourceByProjectSQL(Number(req.params.projectId)); + const getSurveyFundingSourcesSQLStatement = queries.project.getFundingSourceByProjectSQL( + Number(req.params.projectId) + ); if (!getSurveyFundingSourcesSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/permits/list.test.ts b/api/src/paths/project/{projectId}/survey/permits/list.test.ts index 0c3a08edbb..184617e320 100644 --- a/api/src/paths/project/{projectId}/survey/permits/list.test.ts +++ b/api/src/paths/project/{projectId}/survey/permits/list.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as list from './list'; -import * as db from '../../../../../database/db'; -import * as survey_view_queries from '../../../../../queries/survey/survey-view-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import survey_queries from '../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as list from './list'; chai.use(sinonChai); @@ -54,8 +55,8 @@ describe('getSurveyPermits', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -67,7 +68,7 @@ describe('getSurveyPermits', () => { } }); - sinon.stub(survey_view_queries, 'getAllAssignablePermitsForASurveySQL').returns(null); + sinon.stub(survey_queries, 'getAllAssignablePermitsForASurveySQL').returns(null); try { const result = list.getSurveyPermits(); @@ -75,8 +76,8 @@ describe('getSurveyPermits', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -104,7 +105,7 @@ describe('getSurveyPermits', () => { query: mockQuery }); - sinon.stub(survey_view_queries, 'getAllAssignablePermitsForASurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getAllAssignablePermitsForASurveySQL').returns(SQL`some query`); const result = list.getSurveyPermits(); @@ -126,7 +127,7 @@ describe('getSurveyPermits', () => { query: mockQuery }); - sinon.stub(survey_view_queries, 'getAllAssignablePermitsForASurveySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getAllAssignablePermitsForASurveySQL').returns(SQL`some query`); const result = list.getSurveyPermits(); diff --git a/api/src/paths/project/{projectId}/survey/permits/list.ts b/api/src/paths/project/{projectId}/survey/permits/list.ts index 132bfdd4f1..7a4bd5b317 100644 --- a/api/src/paths/project/{projectId}/survey/permits/list.ts +++ b/api/src/paths/project/{projectId}/survey/permits/list.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { getAllAssignablePermitsForASurveySQL } from '../../../../../queries/survey/survey-view-queries'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/permits/list'); -export const GET: Operation = [getSurveyPermits()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyPermits() +]; GET.apiDoc = { description: 'Fetches a list of permits for a survey based on a project.', tags: ['permits'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -74,7 +86,9 @@ export function getSurveyPermits(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveyPermitsSQLStatement = getAllAssignablePermitsForASurveySQL(Number(req.params.projectId)); + const getSurveyPermitsSQLStatement = queries.survey.getAllAssignablePermitsForASurveySQL( + Number(req.params.projectId) + ); if (!getSurveyPermitsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.test.ts index 246cf20f08..184bde4958 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as listAttachments from './list'; -import * as db from '../../../../../../database/db'; -import * as survey_attachments_queries from '../../../../../../queries/survey/survey-attachments-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../database/db'; +import { HTTPError } from '../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../../__mocks__/db'; +import * as listAttachments from './list'; chai.use(sinonChai); @@ -50,8 +51,8 @@ describe('lists the survey attachments', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -63,7 +64,7 @@ describe('lists the survey attachments', () => { } }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(null); try { const result = listAttachments.getSurveyAttachments(); @@ -71,8 +72,8 @@ describe('lists the survey attachments', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -117,7 +118,7 @@ describe('lists the survey attachments', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getSurveyAttachments(); @@ -186,7 +187,7 @@ describe('lists the survey attachments', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getSurveyAttachments(); @@ -227,7 +228,7 @@ describe('lists the survey attachments', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); const result = listAttachments.getSurveyAttachments(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.ts index 10f2c5947e..a6e66b8d46 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/list.ts @@ -1,27 +1,36 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../database/db'; -import { HTTP400 } from '../../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../../errors/custom-error'; import { GetAttachmentsData } from '../../../../../../models/project-survey-attachments'; -import { - getSurveyAttachmentsSQL, - getSurveyReportAttachmentsSQL -} from '../../../../../../queries/survey/survey-attachments-queries'; +import { queries } from '../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/list'); -export const GET: Operation = [getSurveyAttachments()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyAttachments() +]; GET.apiDoc = { description: 'Fetches a list of attachments of a survey.', tags: ['attachments'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -48,17 +57,23 @@ GET.apiDoc = { content: { 'application/json': { schema: { - type: 'array', - items: { - type: 'object', - properties: { - fileName: { - description: 'The file name of the attachment', - type: 'string' - }, - lastModified: { - description: 'The date the object was last modified', - type: 'string' + type: 'object', + required: ['attachmentsList'], + properties: { + attachmentsList: { + type: 'array', + items: { + type: 'object', + properties: { + fileName: { + description: 'The file name of the attachment', + type: 'string' + }, + lastModified: { + description: 'The date the object was last modified', + type: 'string' + } + } } } } @@ -66,9 +81,18 @@ GET.apiDoc = { } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } @@ -86,8 +110,10 @@ export function getSurveyAttachments(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveyAttachmentsSQLStatement = getSurveyAttachmentsSQL(Number(req.params.surveyId)); - const getSurveyReportAttachmentsSQLStatement = getSurveyReportAttachmentsSQL(Number(req.params.surveyId)); + const getSurveyAttachmentsSQLStatement = queries.survey.getSurveyAttachmentsSQL(Number(req.params.surveyId)); + const getSurveyReportAttachmentsSQLStatement = queries.survey.getSurveyReportAttachmentsSQL( + Number(req.params.surveyId) + ); if (!getSurveyAttachmentsSQLStatement || !getSurveyReportAttachmentsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.test.ts new file mode 100644 index 0000000000..8408142c76 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.test.ts @@ -0,0 +1,191 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import * as file_utils from '../../../../../../../utils/file-utils'; +import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as upload from './upload'; + +chai.use(sinonChai); + +describe('uploadMedia', () => { + afterEach(() => { + sinon.restore(); + }); + + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + params: { + projectId: 1, + surveyId: 1 + }, + files: [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ], + body: { + attachmentType: 'Report' + }, + auth_payload: { + preferred_username: 'user', + email: 'email@example.com' + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + it('should throw an error when projectId is missing', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = upload.uploadMedia(); + + await result( + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing projectId'); + } + }); + + it('should throw an error when surveyId is missing', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = upload.uploadMedia(); + + await result( + { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing surveyId'); + } + }); + + it('should throw an error when files are missing', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = upload.uploadMedia(); + + await result({ ...sampleReq, files: [] }, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); + } + }); + + it('should throw a 400 error when file format incorrect', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + + try { + const result = upload.uploadMedia(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey attachment data'); + } + }); + + it('should throw a 400 error when file contains malicious content', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); + sinon.stub(upload, 'upsertSurveyReportAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); + sinon.stub(file_utils, 'scanFileForVirus').resolves(false); + + try { + const result = upload.uploadMedia(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); + } + }); + + it('should return id and revision_count on success (with username and email)', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); + sinon.stub(upload, 'upsertSurveyReportAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + + const result = upload.uploadMedia(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); + }); + + it('should return id and revision_count on success (without username and email)', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); + sinon.stub(upload, 'upsertSurveyReportAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + + const result = upload.uploadMedia(); + + await result( + { ...sampleReq, auth_payload: { ...sampleReq.auth_payload, preferred_username: null, email: null } }, + sampleRes as any, + (null as unknown) as any + ); + + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); + }); +}); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.ts new file mode 100644 index 0000000000..2e69e31106 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/report/upload.ts @@ -0,0 +1,357 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { + IReportAttachmentAuthor, + PostReportAttachmentMetadata, + PutReportAttachmentMetadata +} from '../../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; +import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils'; +import { getLogger } from '../../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/report/upload'); + +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + uploadMedia() +]; +POST.apiDoc = { + description: 'Upload a survey-specific report.', + tags: ['attachment'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + required: true + }, + { + in: 'path', + name: 'surveyId', + required: true + } + ], + requestBody: { + description: 'Attachment upload post request object.', + content: { + 'multipart/form-data': { + schema: { + type: 'object', + required: ['media', 'attachmentMeta'], + properties: { + media: { + type: 'string', + format: 'binary' + }, + attachmentMeta: { + type: 'object', + required: ['title', 'year_published', 'authors', 'description'], + properties: { + title: { + type: 'string' + }, + year_published: { + type: 'string', + description: + 'Year the report is published. (Note: Content-Type: multipart/form-data requires all parameters to be strings.)' + }, + authors: { + type: 'array', + items: { + type: 'object', + required: ['first_name', 'last_name'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + }, + description: { + type: 'string' + } + } + } + } + } + } + } + }, + responses: { + 200: { + description: 'Report upload response.', + content: { + 'application/json': { + schema: { + type: 'object', + description: 'The S3 unique key for this file.', + required: ['attachmentId', 'revision_count'], + properties: { + attachmentId: { + type: 'number' + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Uploads any media in the request to S3, adding their keys to the request. + * Also adds the metadata to the survey_attachment DB table + * Does nothing if no media is present in the request. + * + * @returns {RequestHandler} + */ +export function uploadMedia(): RequestHandler { + return async (req, res) => { + const rawMediaArray: Express.Multer.File[] = req.files as Express.Multer.File[]; + + if (!req.params.projectId) { + throw new HTTP400('Missing projectId'); + } + + if (!req.params.surveyId) { + throw new HTTP400('Missing surveyId'); + } + + if (!rawMediaArray || !rawMediaArray.length) { + // no media objects included, skipping media upload step + throw new HTTP400('Missing upload data'); + } + + if (!req.body) { + throw new HTTP400('Missing request body'); + } + + const rawMediaFile: Express.Multer.File = rawMediaArray[0]; + + defaultLog.debug({ + label: 'uploadMedia', + message: 'files', + files: { ...rawMediaFile, buffer: 'Too big to print' } + }); + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + // Scan file for viruses using ClamAV + const virusScanResult = await scanFileForVirus(rawMediaFile); + + if (!virusScanResult) { + throw new HTTP400('Malicious content detected, upload cancelled'); + } + + const upsertResult = await upsertSurveyReportAttachment( + rawMediaFile, + Number(req.params.projectId), + Number(req.params.surveyId), + req.body.attachmentMeta, + connection + ); + + const metadata = { + filename: rawMediaFile.originalname, + username: (req['auth_payload'] && req['auth_payload'].preferred_username) || '', + email: (req['auth_payload'] && req['auth_payload'].email) || '' + }; + + const result = await uploadFileToS3(rawMediaFile, upsertResult.key, metadata); + + defaultLog.debug({ label: 'uploadMedia', message: 'result', result }); + + await connection.commit(); + + return res.status(200).json({ attachmentId: upsertResult.id, revision_count: upsertResult.revision_count }); + } catch (error) { + defaultLog.error({ label: 'uploadMedia', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const upsertSurveyReportAttachment = async ( + file: Express.Multer.File, + projectId: number, + surveyId: number, + attachmentMeta: any, + connection: IDBConnection +): Promise<{ id: number; revision_count: number; key: string }> => { + const getSqlStatement = queries.survey.getSurveyReportAttachmentByFileNameSQL(surveyId, file.originalname); + + if (!getSqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const key = generateS3FileKey({ + projectId: projectId, + surveyId: surveyId, + fileName: file.originalname, + folder: 'reports' + }); + + const getResponse = await connection.query(getSqlStatement.text, getSqlStatement.values); + + let metadata; + let attachmentResult: { id: number; revision_count: number }; + + if (getResponse && getResponse.rowCount > 0) { + // Existing attachment with matching name found, update it + metadata = new PutReportAttachmentMetadata(attachmentMeta); + attachmentResult = await updateSurveyReportAttachment(file, surveyId, metadata, connection); + } else { + // No matching attachment found, insert new attachment + metadata = new PostReportAttachmentMetadata(attachmentMeta); + attachmentResult = await insertSurveyReportAttachment( + file, + surveyId, + new PostReportAttachmentMetadata(attachmentMeta), + key, + connection + ); + } + + // Delete any existing attachment author records + await deleteSurveyReportAttachmentAuthors(attachmentResult.id, connection); + + const promises = []; + + // Insert any new attachment author records + promises.push( + metadata.authors.map((author) => insertSurveyReportAttachmentAuthor(attachmentResult.id, author, connection)) + ); + + await Promise.all(promises); + + return { ...attachmentResult, key }; +}; + +export const insertSurveyReportAttachment = async ( + file: Express.Multer.File, + surveyId: number, + attachmentMeta: PostReportAttachmentMetadata, + key: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.survey.postSurveyReportAttachmentSQL( + file.originalname, + file.size, + surveyId, + key, + attachmentMeta + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to insert survey attachment data'); + } + + return response.rows[0]; +}; + +export const updateSurveyReportAttachment = async ( + file: Express.Multer.File, + surveyId: number, + attachmentMeta: PutReportAttachmentMetadata, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.survey.putSurveyReportAttachmentSQL(surveyId, file.originalname, attachmentMeta); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to update survey attachment data'); + } + + return response.rows[0]; +}; + +export const deleteSurveyReportAttachmentAuthors = async ( + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.deleteSurveyReportAttachmentAuthorsSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete attachment report authors statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP400('Failed to delete attachment report authors records'); + } +}; + +export const insertSurveyReportAttachmentAuthor = async ( + attachmentId: number, + author: IReportAttachmentAuthor, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.insertSurveyReportAttachmentAuthorSQL(attachmentId, author); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert attachment report author statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to insert attachment report author record'); + } +}; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.test.ts index 3a33163333..8e2db16132 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.test.ts @@ -2,12 +2,13 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as upload from './upload'; +import SQL from 'sql-template-strings'; import * as db from '../../../../../../database/db'; +import { HTTPError } from '../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../queries/survey'; import * as file_utils from '../../../../../../utils/file-utils'; -import * as survey_attachment_queries from '../../../../../../queries/survey/survey-attachments-queries'; -import SQL from 'sql-template-strings'; import { getMockDBConnection } from '../../../../../../__mocks__/db'; +import * as upload from './upload'; chai.use(sinonChai); @@ -22,8 +23,7 @@ describe('uploadMedia', () => { keycloak_token: {}, params: { projectId: 1, - surveyId: 1, - attachmentId: 2 + surveyId: 1 }, files: [ { @@ -35,7 +35,7 @@ describe('uploadMedia', () => { } ], body: { - attachmentType: 'Image' + attachmentType: 'Other' }, auth_payload: { preferred_username: 'user', @@ -55,53 +55,53 @@ describe('uploadMedia', () => { } }; - it('should throw an error when surveyId is missing', async () => { + it('should throw an error when projectId is missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { const result = upload.uploadMedia(); await result( - { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, (null as unknown) as any, (null as unknown) as any ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing surveyId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing projectId'); } }); - it('should throw an error when files are missing', async () => { + it('should throw an error when surveyId is missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { const result = upload.uploadMedia(); - await result({ ...sampleReq, files: [] }, (null as unknown) as any, (null as unknown) as any); + await result( + { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing upload data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing surveyId'); } }); - it('should throw an error when attachmentType is missing', async () => { + it('should throw an error when files are missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { const result = upload.uploadMedia(); - await result( - { ...sampleReq, body: { attachmentType: null } }, - (null as unknown) as any, - (null as unknown) as any - ); + await result({ ...sampleReq, files: [] }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing attachment file type'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); } }); @@ -121,8 +121,8 @@ describe('uploadMedia', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey attachment data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey attachment data'); } }); @@ -135,7 +135,7 @@ describe('uploadMedia', () => { }); sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); - sinon.stub(upload, 'upsertSurveyAttachment').resolves(1); + sinon.stub(upload, 'upsertSurveyAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); sinon.stub(file_utils, 'scanFileForVirus').resolves(false); try { @@ -144,12 +144,12 @@ describe('uploadMedia', () => { await result(sampleReq, sampleRes as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Malicious content detected, upload cancelled'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); } }); - it('should return file key on success (with username and email)', async () => { + it('should return id and revision_count on success (with username and email)', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -158,17 +158,17 @@ describe('uploadMedia', () => { }); sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); - sinon.stub(upload, 'upsertSurveyAttachment').resolves(1); + sinon.stub(upload, 'upsertSurveyAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); const result = upload.uploadMedia(); await result(sampleReq, sampleRes as any, (null as unknown) as any); - expect(actualResult).to.eql('1/1/test.txt'); + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); }); - it('should return file key on success (without username and email)', async () => { + it('should return id and revision_count on success (without username and email)', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -177,7 +177,7 @@ describe('uploadMedia', () => { }); sinon.stub(file_utils, 'uploadFileToS3').resolves({ Key: '1/1/test.txt' } as any); - sinon.stub(upload, 'upsertSurveyAttachment').resolves(1); + sinon.stub(upload, 'upsertSurveyAttachment').resolves({ id: 1, revision_count: 0, key: '1/1/test.txt' }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); const result = upload.uploadMedia(); @@ -188,7 +188,7 @@ describe('uploadMedia', () => { (null as unknown) as any ); - expect(actualResult).to.eql('1/1/test.txt'); + expect(actualResult).to.eql({ attachmentId: 1, revision_count: 0 }); }); }); @@ -216,15 +216,15 @@ describe('upsertSurveyAttachment', () => { const attachmentType = 'Image'; it('should throw an error when failed to generate SQL get statement', async () => { - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(null); try { await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -235,8 +235,8 @@ describe('upsertSurveyAttachment', () => { rowCount: 1 }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'putSurveyAttachmentSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyAttachmentSQL').returns(null); try { await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { @@ -246,8 +246,8 @@ describe('upsertSurveyAttachment', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); @@ -264,8 +264,8 @@ describe('upsertSurveyAttachment', () => { rowCount: null }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'putSurveyAttachmentSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyAttachmentSQL').returns(SQL`something`); try { await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { @@ -275,12 +275,12 @@ describe('upsertSurveyAttachment', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to update survey attachment data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update survey attachment data'); } }); - it('should return the rowCount of records updated on success (update)', async () => { + it('should return the id, revision_count of records updated on success (update)', async () => { const mockQuery = sinon.stub(); mockQuery @@ -290,18 +290,19 @@ describe('upsertSurveyAttachment', () => { }) .onSecondCall() .resolves({ - rowCount: 1 + rowCount: 1, + rows: [{ id: 1, revision_count: 0 }] }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'putSurveyAttachmentSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyAttachmentSQL').returns(SQL`something`); const result = await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { ...dbConnectionObj, query: mockQuery }); - expect(result).to.equal(1); + expect(result).to.eql({ id: 1, revision_count: 0, key: 'projects/1/surveys/2/test.txt' }); }); it('should throw an error when failed to generate SQL insert statement', async () => { @@ -311,8 +312,8 @@ describe('upsertSurveyAttachment', () => { rowCount: null }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'postSurveyAttachmentSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveyAttachmentSQL').returns(null); try { await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { @@ -322,12 +323,12 @@ describe('upsertSurveyAttachment', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); - it('should throw an error when insert result has no id', async () => { + it('should throw an error when insert result has no rows', async () => { const mockQuery = sinon.stub(); mockQuery @@ -337,11 +338,11 @@ describe('upsertSurveyAttachment', () => { }) .onSecondCall() .resolves({ - rows: [{ id: null }] + rows: [] }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'postSurveyAttachmentSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveyAttachmentSQL').returns(SQL`something`); try { await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { @@ -351,12 +352,12 @@ describe('upsertSurveyAttachment', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey attachment data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey attachment data'); } }); - it('should return the id of record inserted on success (insert)', async () => { + it('should return the id and revision_count of record inserted on success (insert)', async () => { const mockQuery = sinon.stub(); mockQuery @@ -366,17 +367,17 @@ describe('upsertSurveyAttachment', () => { }) .onSecondCall() .resolves({ - rows: [{ id: 12 }] + rows: [{ id: 12, revision_count: 0, key: 'projects/1/surveys/2/test.txt' }] }); - sinon.stub(survey_attachment_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); - sinon.stub(survey_attachment_queries, 'postSurveyAttachmentSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentByFileNameSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'postSurveyAttachmentSQL').returns(SQL`something`); const result = await upload.upsertSurveyAttachment(file, projectId, surveyId, attachmentType, { ...dbConnectionObj, query: mockQuery }); - expect(result).to.equal(12); + expect(result).to.eql({ id: 12, revision_count: 0, key: 'projects/1/surveys/2/test.txt' }); }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.ts index 42f8123c8b..2da518b052 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/upload.ts @@ -1,28 +1,36 @@ -'use strict'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../constants/roles'; +import { ATTACHMENT_TYPE } from '../../../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../../database/db'; -import { HTTP400 } from '../../../../../../errors/CustomError'; -import { - getSurveyAttachmentByFileNameSQL, - postSurveyAttachmentSQL, - postSurveyReportAttachmentSQL, - putSurveyAttachmentSQL, - putSurveyReportAttachmentSQL -} from '../../../../../../queries/survey/survey-attachments-queries'; +import { HTTP400 } from '../../../../../../errors/custom-error'; +import { queries } from '../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../request-handlers/security/authorization'; import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/upload'); -export const POST: Operation = [uploadMedia()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + uploadMedia() +]; POST.apiDoc = { description: 'Upload a survey-specific attachment.', tags: ['attachment'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -43,6 +51,7 @@ POST.apiDoc = { 'multipart/form-data': { schema: { type: 'object', + required: ['media'], properties: { media: { type: 'string', @@ -59,8 +68,16 @@ POST.apiDoc = { content: { 'application/json': { schema: { - type: 'string', - description: 'The S3 unique key for this file.' + type: 'object', + required: ['attachmentId', 'revision_count'], + properties: { + attachmentId: { + type: 'number' + }, + revision_count: { + type: 'number' + } + } } } } @@ -79,23 +96,25 @@ POST.apiDoc = { * Also adds the metadata to the survey_attachment DB table * Does nothing if no media is present in the request. * - * TODO: make media handling an extension that can be added to different endpoints/record types - * * @returns {RequestHandler} */ export function uploadMedia(): RequestHandler { return async (req, res) => { const rawMediaArray: Express.Multer.File[] = req.files as Express.Multer.File[]; + if (!req.params.projectId) { + throw new HTTP400('Missing projectId'); + } + + if (!req.params.surveyId) { + throw new HTTP400('Missing surveyId'); + } + if (!rawMediaArray || !rawMediaArray.length) { // no media objects included, skipping media upload step throw new HTTP400('Missing upload data'); } - if (!req.body || !req.body.attachmentType) { - throw new HTTP400('Missing attachment file type'); - } - const rawMediaFile: Express.Multer.File = rawMediaArray[0]; defaultLog.debug({ @@ -104,10 +123,6 @@ export function uploadMedia(): RequestHandler { files: { ...rawMediaFile, buffer: 'Too big to print' } }); - if (!req.params.surveyId) { - throw new HTTP400('Missing surveyId'); - } - const connection = getDBConnection(req['keycloak_token']); try { @@ -120,35 +135,27 @@ export function uploadMedia(): RequestHandler { throw new HTTP400('Malicious content detected, upload cancelled'); } - // Insert file metadata into survey_attachment or survey_report_attachment table - await upsertSurveyAttachment( + const upsertResult = await upsertSurveyAttachment( rawMediaFile, Number(req.params.projectId), Number(req.params.surveyId), - req.body.attachmentType, + ATTACHMENT_TYPE.OTHER, connection ); - // Upload file to S3 - const key = generateS3FileKey({ - projectId: Number(req.params.projectId), - surveyId: Number(req.params.surveyId), - fileName: rawMediaFile.originalname - }); - const metadata = { filename: rawMediaFile.originalname, username: (req['auth_payload'] && req['auth_payload'].preferred_username) || '', email: (req['auth_payload'] && req['auth_payload'].email) || '' }; - const result = await uploadFileToS3(rawMediaFile, key, metadata); + const result = await uploadFileToS3(rawMediaFile, upsertResult.key, metadata); defaultLog.debug({ label: 'uploadMedia', message: 'result', result }); await connection.commit(); - return res.status(200).json(result.Key); + return res.status(200).json({ attachmentId: upsertResult.id, revision_count: upsertResult.revision_count }); } catch (error) { defaultLog.error({ label: 'uploadMedia', message: 'error', error }); await connection.rollback(); @@ -165,52 +172,82 @@ export const upsertSurveyAttachment = async ( surveyId: number, attachmentType: string, connection: IDBConnection -): Promise => { - const getSqlStatement = getSurveyAttachmentByFileNameSQL(surveyId, file.originalname); +): Promise<{ id: number; revision_count: number; key: string }> => { + const getSqlStatement = queries.survey.getSurveyAttachmentByFileNameSQL(surveyId, file.originalname); if (!getSqlStatement) { throw new HTTP400('Failed to build SQL get statement'); } + const key = generateS3FileKey({ projectId: projectId, surveyId: surveyId, fileName: file.originalname }); + const getResponse = await connection.query(getSqlStatement.text, getSqlStatement.values); + let attachmentResult: { id: number; revision_count: number }; + if (getResponse && getResponse.rowCount > 0) { - const updateSqlStatement = - attachmentType === 'Report' - ? putSurveyReportAttachmentSQL(surveyId, file.originalname) - : putSurveyAttachmentSQL(surveyId, file.originalname, attachmentType); + // Existing attachment with matching name found, update it + attachmentResult = await updateSurveyAttachment(file, surveyId, attachmentType, connection); + } else { + // No matching attachment found, insert new attachment + attachmentResult = await insertSurveyAttachment(file, projectId, surveyId, attachmentType, connection); + } - if (!updateSqlStatement) { - throw new HTTP400('Failed to build SQL update statement'); - } + return { ...attachmentResult, key }; +}; - const updateResponse = await connection.query(updateSqlStatement.text, updateSqlStatement.values); - const updateResult = (updateResponse && updateResponse.rowCount) || null; +export const insertSurveyAttachment = async ( + file: Express.Multer.File, + projectId: number, + surveyId: number, + attachmentType: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const key = generateS3FileKey({ + projectId: projectId, + surveyId: surveyId, + fileName: file.originalname, + folder: 'reports' + }); + + const sqlStatement = queries.survey.postSurveyAttachmentSQL( + file.originalname, + file.size, + attachmentType, + surveyId, + key + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } - if (!updateResult) { - throw new HTTP400('Failed to update survey attachment data'); - } + const response = await connection.query(sqlStatement.text, sqlStatement.values); - return updateResult; + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to insert survey attachment data'); } - const key = generateS3FileKey({ projectId: projectId, surveyId: surveyId, fileName: file.originalname }); + return response.rows[0]; +}; - const insertSqlStatement = - attachmentType === 'Report' - ? postSurveyReportAttachmentSQL(file.originalname, file.size, projectId, surveyId, key) - : postSurveyAttachmentSQL(file.originalname, file.size, attachmentType, projectId, surveyId, key); +export const updateSurveyAttachment = async ( + file: Express.Multer.File, + surveyId: number, + attachmentType: string, + connection: IDBConnection +): Promise<{ id: number; revision_count: number }> => { + const sqlStatement = queries.survey.putSurveyAttachmentSQL(surveyId, file.originalname, attachmentType); - if (!insertSqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); } - const insertResponse = await connection.query(insertSqlStatement.text, insertSqlStatement.values); - const insertResult = (insertResponse && insertResponse.rows && insertResponse.rows[0]) || null; + const response = await connection.query(sqlStatement.text, sqlStatement.values); - if (!insertResult || !insertResult.id) { - throw new HTTP400('Failed to insert survey attachment data'); + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to update survey attachment data'); } - return insertResult.id; + return response.rows[0]; }; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.test.ts index b03aed95f4..29b81d14d3 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.test.ts @@ -1,15 +1,16 @@ +import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as delete_attachment from './delete'; -import * as db from '../../../../../../../database/db'; -import * as survey_attachments_queries from '../../../../../../../queries/survey/survey-attachments-queries'; -import * as security_queries from '../../../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import security_queries from '../../../../../../../queries/security'; +import survey_queries from '../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../utils/file-utils'; -import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as delete_attachment from './delete'; chai.use(sinonChai); @@ -40,6 +41,9 @@ describe('deleteAttachment', () => { return { json: (result: any) => { actualResult = result; + }, + send: () => { + //do nothing } }; } @@ -58,8 +62,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -76,8 +80,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -94,8 +98,8 @@ describe('deleteAttachment', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `attachmentType`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `attachmentType`'); } }); @@ -115,8 +119,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL unsecure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL unsecure record statement'); } }); @@ -141,8 +145,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to unsecure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to unsecure record'); } }); @@ -160,7 +164,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(survey_attachments_queries, 'deleteSurveyAttachmentSQL').returns(null); + sinon.stub(survey_queries, 'deleteSurveyAttachmentSQL').returns(null); try { const result = delete_attachment.deleteAttachment(); @@ -168,8 +172,8 @@ describe('deleteAttachment', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete project attachment statement'); } }); @@ -180,7 +184,7 @@ describe('deleteAttachment', () => { .onFirstCall() .resolves({ rowCount: 1 }) .onSecondCall() - .resolves({ rows: [{ key: 's3Key' }] }); + .resolves({ rowCount: 1, rows: [{ key: 's3Key' }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -191,7 +195,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(survey_attachments_queries, 'deleteSurveyAttachmentSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves(null); const result = delete_attachment.deleteAttachment(); @@ -201,7 +205,7 @@ describe('deleteAttachment', () => { expect(actualResult).to.equal(null); }); - it('should return the rowCount response on success when type is not Report', async () => { + it('should return null response on success when type is not Report', async () => { const mockQuery = sinon.stub(); mockQuery @@ -219,23 +223,25 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(survey_attachments_queries, 'deleteSurveyAttachmentSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves('non null response' as DeleteObjectOutput); const result = delete_attachment.deleteAttachment(); await result(sampleReq, sampleRes as any, (null as unknown) as any); - expect(actualResult).to.equal(1); + expect(actualResult).to.equal(null); }); - it('should return the rowCount response on success when type is Report', async () => { + it('should return null response on success when type is Report', async () => { const mockQuery = sinon.stub(); mockQuery .onFirstCall() .resolves({ rowCount: 1 }) .onSecondCall() + .resolves({ rowCount: 1 }) + .onThirdCall() .resolves({ rows: [{ key: 's3Key' }], rowCount: 1 }); sinon.stub(db, 'getDBConnection').returns({ @@ -247,7 +253,7 @@ describe('deleteAttachment', () => { }); sinon.stub(security_queries, 'unsecureAttachmentRecordSQL').returns(SQL`something`); - sinon.stub(survey_attachments_queries, 'deleteSurveyReportAttachmentSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyReportAttachmentSQL').returns(SQL`some query`); sinon.stub(file_utils, 'deleteFileFromS3').resolves('non null response' as DeleteObjectOutput); const result = delete_attachment.deleteAttachment(); @@ -258,6 +264,6 @@ describe('deleteAttachment', () => { (null as unknown) as any ); - expect(actualResult).to.equal(1); + expect(actualResult).to.equal(null); }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.ts index a50e12df2e..64e427ef90 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete.ts @@ -1,21 +1,32 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { unsecureAttachmentRecordSQL } from '../../../../../../../queries/security/security-queries'; +import { ATTACHMENT_TYPE } from '../../../../../../../constants/attachments'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; -import { - deleteSurveyAttachmentSQL, - deleteSurveyReportAttachmentSQL -} from '../../../../../../../queries/survey/survey-attachments-queries'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { deleteFileFromS3 } from '../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../utils/logger'; import { attachmentApiDocObject } from '../../../../../../../utils/shared-api-docs'; +import { deleteSurveyReportAttachmentAuthors } from '../report/upload'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/delete'); -export const POST: Operation = [deleteAttachment()]; +export const POST: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteAttachment() +]; POST.apiDoc = { ...attachmentApiDocObject('Delete an attachment of a survey.', 'Row count of successfully deleted attachment record'), @@ -54,6 +65,26 @@ POST.apiDoc = { } } } + }, + responses: { + 200: { + description: 'Delete an attachment of a survey OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } } }; @@ -83,31 +114,24 @@ export function deleteAttachment(): RequestHandler { await unsecureSurveyAttachmentRecord(req.body.securityToken, req.body.attachmentType, connection); } - // Proceed to delete the attachment record itself - const deleteSurveyAttachmentSQLStatement = - req.body.attachmentType === 'Report' - ? deleteSurveyReportAttachmentSQL(Number(req.params.attachmentId)) - : deleteSurveyAttachmentSQL(Number(req.params.attachmentId)); + let deleteResult: { key: string }; + if (req.body.attachmentType === ATTACHMENT_TYPE.REPORT) { + await deleteSurveyReportAttachmentAuthors(Number(req.params.attachmentId), connection); - if (!deleteSurveyAttachmentSQLStatement) { - throw new HTTP400('Failed to build SQL delete statement'); + deleteResult = await deleteSurveyReportAttachment(Number(req.params.attachmentId), connection); + } else { + deleteResult = await deleteSurveyAttachment(Number(req.params.attachmentId), connection); } - const result = await connection.query( - deleteSurveyAttachmentSQLStatement.text, - deleteSurveyAttachmentSQLStatement.values - ); - const s3Key = result && result.rows.length && result.rows[0].key; - await connection.commit(); - const deleteFileResult = await deleteFileFromS3(s3Key); + const deleteFileResult = await deleteFileFromS3(deleteResult.key); if (!deleteFileResult) { return res.status(200).json(null); } - return res.status(200).json(result && result.rowCount); + return res.status(200).send(); } catch (error) { defaultLog.error({ label: 'deleteAttachment', message: 'error', error }); await connection.rollback(); @@ -125,8 +149,8 @@ const unsecureSurveyAttachmentRecord = async ( ): Promise => { const unsecureRecordSQLStatement = attachmentType === 'Report' - ? unsecureAttachmentRecordSQL('survey_report_attachment', securityToken) - : unsecureAttachmentRecordSQL('survey_attachment', securityToken); + ? queries.security.unsecureAttachmentRecordSQL('survey_report_attachment', securityToken) + : queries.security.unsecureAttachmentRecordSQL('survey_attachment', securityToken); if (!unsecureRecordSQLStatement) { throw new HTTP400('Failed to build SQL unsecure record statement'); @@ -141,3 +165,41 @@ const unsecureSurveyAttachmentRecord = async ( throw new HTTP400('Failed to unsecure record'); } }; + +export const deleteSurveyAttachment = async ( + attachmentId: number, + connection: IDBConnection +): Promise<{ key: string }> => { + const sqlStatement = queries.survey.deleteSurveyAttachmentSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete project attachment statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to delete survey attachment record'); + } + + return response.rows[0]; +}; + +export const deleteSurveyReportAttachment = async ( + attachmentId: number, + connection: IDBConnection +): Promise<{ key: string }> => { + const sqlStatement = queries.survey.deleteSurveyReportAttachmentSQL(attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete project report attachment statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to delete survey attachment report record'); + } + + return response.rows[0]; +}; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.test.ts index f03848f944..e6b1524bca 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.test.ts @@ -2,16 +2,18 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_signed_url from './getSignedUrl'; -import * as db from '../../../../../../../database/db'; -import * as survey_attachments_queries from '../../../../../../../queries/survey/survey-attachments-queries'; import SQL from 'sql-template-strings'; +import { ATTACHMENT_TYPE } from '../../../../../../../constants/attachments'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as get_signed_url from './getSignedUrl'; chai.use(sinonChai); -describe('getSingleAttachmentURL', () => { +describe('getSurveyAttachmentSignedURL', () => { afterEach(() => { sinon.restore(); }); @@ -24,6 +26,9 @@ describe('getSingleAttachmentURL', () => { projectId: 1, surveyId: 1, attachmentId: 2 + }, + query: { + attachmentType: 'Other' } } as any; @@ -43,7 +48,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getSurveyAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, @@ -52,8 +57,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -61,7 +66,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getSurveyAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, @@ -70,29 +75,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentS3KeySQL').returns(null); - - try { - const result = get_signed_url.getSingleAttachmentURL(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -109,36 +93,123 @@ describe('getSingleAttachmentURL', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyAttachmentS3KeySQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves(null); - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getSurveyAttachmentSignedURL(); await result(sampleReq, sampleRes as any, (null as unknown) as any); expect(actualResult).to.equal(null); }); - it('should return the signed url response on success', async () => { - const mockQuery = sinon.stub(); + describe('non report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); - mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + sinon.stub(survey_queries, 'getSurveyAttachmentS3KeySQL').returns(null); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + try { + const result = get_signed_url.getSurveyAttachmentSignedURL(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build attachment S3 key SQLstatement'); + } }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentS3KeySQL').returns(SQL`some query`); - sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + it('should return the signed url response on success', async () => { + const mockQuery = sinon.stub(); - const result = get_signed_url.getSingleAttachmentURL(); + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getSurveyAttachmentSignedURL(); - expect(actualResult).to.eql('myurlsigned.com'); + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql('myurlsigned.com'); + }); + }); + + describe('report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(survey_queries, 'getSurveyReportAttachmentS3KeySQL').returns(null); + + try { + const result = get_signed_url.getSurveyAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build report attachment S3 key SQLstatement'); + } + }); + + it('should return the signed url response on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyReportAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getSurveyAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + + expect(actualResult).to.eql('myurlsigned.com'); + }); }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.ts index a3560d0abe..3554067202 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl.ts @@ -1,23 +1,39 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; -import { getLogger } from '../../../../../../../utils/logger'; -import { getDBConnection } from '../../../../../../../database/db'; -import { getSurveyAttachmentS3KeySQL } from '../../../../../../../queries/survey/survey-attachments-queries'; +import { ATTACHMENT_TYPE } from '../../../../../../../constants/attachments'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { getS3SignedURL } from '../../../../../../../utils/file-utils'; -import { attachmentApiDocObject } from '../../../../../../../utils/shared-api-docs'; +import { getLogger } from '../../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/getSignedUrl'); -export const GET: Operation = [getSingleAttachmentURL()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyAttachmentSignedURL() +]; GET.apiDoc = { - ...attachmentApiDocObject( - 'Retrieves the signed url of an attachment in a survey by its file name.', - 'GET response containing the signed url of an attachment.' - ), + description: 'Retrieves the signed url of a survey attachment.', + tags: ['attachment'], + security: [ + { + Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR] + } + ], parameters: [ { in: 'path', @@ -42,13 +58,55 @@ GET.apiDoc = { type: 'number' }, required: true + }, + { + in: 'query', + name: 'attachmentType', + schema: { + type: 'string', + enum: ['Report', 'Other'] + }, + required: true } - ] + ], + responses: { + 200: { + description: 'Response containing the signed url of an attachment.', + content: { + 'text/plain': { + schema: { + type: 'string' + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } }; -export function getSingleAttachmentURL(): RequestHandler { +export function getSurveyAttachmentSignedURL(): RequestHandler { return async (req, res) => { - defaultLog.debug({ label: 'Get single attachment url', message: 'params', req_params: req.params }); + defaultLog.debug({ + label: 'getSurveyAttachmentSignedURL', + message: 'params', + req_params: req.params, + req_query: req.query, + req_body: req.body + }); if (!req.params.surveyId) { throw new HTTP400('Missing required path param `surveyId`'); @@ -58,29 +116,31 @@ export function getSingleAttachmentURL(): RequestHandler { throw new HTTP400('Missing required path param `attachmentId`'); } + if (!req.query.attachmentType) { + throw new HTTP400('Missing required query param `attachmentType`'); + } + const connection = getDBConnection(req['keycloak_token']); try { - const getSurveyAttachmentS3KeySQLStatement = getSurveyAttachmentS3KeySQL( - Number(req.params.surveyId), - Number(req.params.attachmentId) - ); - - if (!getSurveyAttachmentS3KeySQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - - const result = await connection.query( - getSurveyAttachmentS3KeySQLStatement.text, - getSurveyAttachmentS3KeySQLStatement.values - ); - + let s3Key; + + if (req.query.attachmentType === ATTACHMENT_TYPE.REPORT) { + s3Key = await getSurveyReportAttachmentS3Key( + Number(req.params.surveyId), + Number(req.params.attachmentId), + connection + ); + } else { + s3Key = await getSurveyAttachmentS3Key( + Number(req.params.surveyId), + Number(req.params.attachmentId), + connection + ); + } await connection.commit(); - const s3Key = result && result.rows.length && result.rows[0].key; - const s3SignedUrl = await getS3SignedURL(s3Key); if (!s3SignedUrl) { @@ -89,7 +149,7 @@ export function getSingleAttachmentURL(): RequestHandler { return res.status(200).json(s3SignedUrl); } catch (error) { - defaultLog.error({ label: 'getSingleAttachmentURL', message: 'error', error }); + defaultLog.error({ label: 'getSurveyAttachmentSignedURL', message: 'error', error }); await connection.rollback(); throw error; } finally { @@ -97,3 +157,43 @@ export function getSingleAttachmentURL(): RequestHandler { } }; } + +export const getSurveyAttachmentS3Key = async ( + surveyId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyAttachmentS3KeySQL(surveyId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; + +export const getSurveyReportAttachmentS3Key = async ( + surveyId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyReportAttachmentS3KeySQL(surveyId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build report attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.test.ts index 2efbeccf51..3e1d7e5e28 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as makeSecure from './makeSecure'; -import * as db from '../../../../../../../database/db'; -import * as security_queries from '../../../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import security_queries from '../../../../../../../queries/security'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as makeSecure from './makeSecure'; chai.use(sinonChai); @@ -54,8 +55,8 @@ describe('makeSurveyAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -72,8 +73,8 @@ describe('makeSurveyAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -90,8 +91,8 @@ describe('makeSurveyAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -108,8 +109,8 @@ describe('makeSurveyAttachmentSecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `attachmentType`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param `attachmentType`'); } }); @@ -123,8 +124,8 @@ describe('makeSurveyAttachmentSecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL secure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL secure record statement'); } }); @@ -144,8 +145,8 @@ describe('makeSurveyAttachmentSecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to secure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to secure record'); } }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.ts index 65f36dcdae..04a15137a8 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../utils/logger'; -import { secureAttachmentRecordSQL } from '../../../../../../../queries/security/security-queries'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeSecure'); -export const PUT: Operation = [makeSurveyAttachmentSecure()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + makeSurveyAttachmentSecure() +]; PUT.apiDoc = { description: 'Make security status of a survey attachment secure.', tags: ['attachment', 'security_status'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -108,12 +120,12 @@ export function makeSurveyAttachmentSecure(): RequestHandler { const secureRecordSQLStatement = req.body.attachmentType === 'Report' - ? secureAttachmentRecordSQL( + ? queries.security.secureAttachmentRecordSQL( Number(req.params.attachmentId), 'survey_report_attachment', Number(req.params.projectId) ) - : secureAttachmentRecordSQL( + : queries.security.secureAttachmentRecordSQL( Number(req.params.attachmentId), 'survey_attachment', Number(req.params.projectId) diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.test.ts index ea5092b338..3c952ca7b4 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as makeUnsecure from './makeUnsecure'; -import * as db from '../../../../../../../database/db'; -import * as security_queries from '../../../../../../../queries/security/security-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import security_queries from '../../../../../../../queries/security'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as makeUnsecure from './makeUnsecure'; chai.use(sinonChai); @@ -55,8 +56,8 @@ describe('makeSurveyAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -73,8 +74,8 @@ describe('makeSurveyAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -91,8 +92,8 @@ describe('makeSurveyAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -105,8 +106,8 @@ describe('makeSurveyAttachmentUnsecure', () => { await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -123,8 +124,8 @@ describe('makeSurveyAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -141,8 +142,8 @@ describe('makeSurveyAttachmentUnsecure', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); @@ -156,8 +157,8 @@ describe('makeSurveyAttachmentUnsecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL unsecure record statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL unsecure record statement'); } }); @@ -177,8 +178,8 @@ describe('makeSurveyAttachmentUnsecure', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to unsecure record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to unsecure record'); } }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.ts index f71a1b0058..2b63545e3f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../utils/logger'; -import { unsecureAttachmentRecordSQL } from '../../../../../../../queries/security/security-queries'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/makeUnsecure'); -export const PUT: Operation = [makeSurveyAttachmentUnsecure()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + makeSurveyAttachmentUnsecure() +]; PUT.apiDoc = { description: 'Make security status of a survey attachment unsecure.', tags: ['attachment', 'security_status'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -108,8 +120,8 @@ export function makeSurveyAttachmentUnsecure(): RequestHandler { const unsecureRecordSQLStatement = req.body.attachmentType === 'Report' - ? unsecureAttachmentRecordSQL('survey_report_attachment', req.body.securityToken) - : unsecureAttachmentRecordSQL('survey_attachment', req.body.securityToken); + ? queries.security.unsecureAttachmentRecordSQL('survey_report_attachment', req.body.securityToken) + : queries.security.unsecureAttachmentRecordSQL('survey_attachment', req.body.securityToken); if (!unsecureRecordSQLStatement) { throw new HTTP400('Failed to build SQL unsecure record statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.test.ts new file mode 100644 index 0000000000..9786ad9b91 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.test.ts @@ -0,0 +1,179 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; +import { getMockDBConnection } from '../../../../../../../../__mocks__/db'; +import * as get_survey_metadata from './get'; + +chai.use(sinonChai); + +describe('gets metadata for a survey report', () => { + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + body: {}, + params: { + projectId: 1, + surveyId: 1, + attachmentId: 1 + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_survey_metadata.getSurveyReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no surveyId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_survey_metadata.getSurveyReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); + } + }); + + it('should throw a 400 error when no attachmentId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_survey_metadata.getSurveyReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectReportAttachmentSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(survey_queries, 'getSurveyReportAttachmentSQL').returns(null); + + try { + const result = get_survey_metadata.getSurveyReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should throw a 400 error when no sql statement returned for getSurveyReportAuthorsSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(survey_queries, 'getSurveyReportAuthorsSQL').returns(null); + + try { + const result = get_survey_metadata.getSurveyReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should return a project report metadata, on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [ + { + attachment_id: 1, + title: 'My report', + update_date: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1' + } + ] + }); + mockQuery.onCall(1).resolves({ rowCount: 1, rows: [{ first_name: 'John', last_name: 'Smith' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyReportAttachmentSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyReportAuthorsSQL').returns(SQL`something`); + + const result = get_survey_metadata.getSurveyReportMetaData(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.be.eql({ + attachment_id: 1, + title: 'My report', + last_modified: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1', + authors: [{ first_name: 'John', last_name: 'Smith' }] + }); + }); +}); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.ts new file mode 100644 index 0000000000..cec171ecf7 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/get.ts @@ -0,0 +1,201 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { getDBConnection } from '../../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { GetReportAttachmentMetadata } from '../../../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/getSignedUrl'); + +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyReportMetaData() +]; + +GET.apiDoc = { + description: 'Retrieves the report metadata of a project attachment if filetype is Report.', + tags: ['attachment'], + security: [ + { + Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'surveyId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Response of the report metadata', + content: { + 'application/json': { + schema: { + type: 'object', + required: [ + 'attachment_id', + 'title', + 'last_modified', + 'description', + 'year_published', + 'revision_count', + 'authors' + ], + properties: { + attachment_id: { + type: 'number' + }, + title: { + type: 'string' + }, + last_modified: { + type: 'string' + }, + description: { + type: 'string' + }, + year_published: { + type: 'number' + }, + revision_count: { + type: 'number' + }, + authors: { + type: 'array', + items: { + type: 'object', + required: ['first_name', 'last_name'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + } + } + } + } + } + }, + + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function getSurveyReportMetaData(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'getSurveyReportMetaData', + message: 'params', + req_params: req.params, + req_query: req.query + }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + if (!req.params.surveyId) { + throw new HTTP400('Missing required path param `surveyId`'); + } + + if (!req.params.attachmentId) { + throw new HTTP400('Missing required path param `attachmentId`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const getProjectReportAttachmentSQLStatement = queries.survey.getSurveyReportAttachmentSQL( + Number(req.params.projectId), + Number(req.params.attachmentId) + ); + + const getProjectReportAuthorsSQLStatement = queries.survey.getSurveyReportAuthorsSQL( + Number(req.params.attachmentId) + ); + + if (!getProjectReportAttachmentSQLStatement || !getProjectReportAuthorsSQLStatement) { + throw new HTTP400('Failed to build metadata SQLStatement'); + } + + await connection.open(); + + const reportMetaData = await connection.query( + getProjectReportAttachmentSQLStatement.text, + getProjectReportAttachmentSQLStatement.values + ); + + const reportAuthorsData = await connection.query( + getProjectReportAuthorsSQLStatement.text, + getProjectReportAuthorsSQLStatement.values + ); + + await connection.commit(); + + const getReportMetaData = reportMetaData && reportMetaData.rows[0]; + + const getReportAuthorsData = reportAuthorsData && reportAuthorsData.rows; + + const reportMetaObj = new GetReportAttachmentMetadata(getReportMetaData, getReportAuthorsData); + + return res.status(200).json(reportMetaObj); + } catch (error) { + defaultLog.error({ label: 'getReportMetadata', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.test.ts new file mode 100644 index 0000000000..c8859abdf0 --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.test.ts @@ -0,0 +1,324 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../../__mocks__/db'; +import * as update_survey_metadata from './update'; + +chai.use(sinonChai); + +describe('updates metadata for a survey report', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '', + surveyId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no surveyId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); + } + }); + + it('should throw a 400 error when no attachmentId is provided', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '1', + attachmentId: '' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); + } + }); + + it('should throw a 400 error when attachment_type is invalid', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'notAReport', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Invalid body param `attachment_type`'); + } + }); + + it('should update a survey report metadata, on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + mockQuery.onCall(1).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); + + it('should throw a 400 error when updateSurveyReportAttachmentMetadataSQL returns null', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [{ id: 1 }] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(survey_queries, 'updateSurveyReportAttachmentMetadataSQL').returns(null); + + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update attachment report statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when the response is null', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '1', + attachmentId: '1' + }; + mockReq.body = { + attachment_type: 'Report', + revision_count: 1, + attachment_meta: { + title: 'My report', + year_published: 2000, + description: 'report abstract', + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ] + } + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: null + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(survey_queries, 'updateSurveyReportAttachmentMetadataSQL').returns(SQL`something`); + + const requestHandler = update_survey_metadata.updateSurveyReportMetadata(); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to update attachment report record'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); +}); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.ts new file mode 100644 index 0000000000..cfa304c74f --- /dev/null +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/attachments/{attachmentId}/metadata/update.ts @@ -0,0 +1,224 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { ATTACHMENT_TYPE } from '../../../../../../../../constants/attachments'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { PutReportAttachmentMetadata } from '../../../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../../../../../utils/logger'; +import { deleteSurveyReportAttachmentAuthors, insertSurveyReportAttachmentAuthor } from '../../report/upload'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/metadata/update'); + +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateSurveyReportMetadata() +]; + +PUT.apiDoc = { + description: 'Update project attachment metadata.', + tags: ['attachment'], + security: [ + { + Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_CREATOR] + } + ], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'surveyId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + description: 'Attachment metadata for attachments of type: Report.', + required: ['attachment_type', 'attachment_meta', 'revision_count'], + properties: { + attachment_type: { + type: 'string', + enum: ['Report'] + }, + attachment_meta: { + type: 'object', + required: ['title', 'year_published', 'authors', 'description'], + properties: { + title: { + type: 'string' + }, + year_published: { + type: 'number' + }, + authors: { + type: 'array', + items: { + type: 'object', + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + }, + description: { + type: 'string' + } + } + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Update project attachment metadata OK' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function updateSurveyReportMetadata(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'updateProjectAttachmentMetadata', + message: 'params', + req_params: req.params, + req_body: req.body + }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.surveyId) { + throw new HTTP400('Missing required path param `surveyId`'); + } + + if (!req.params.attachmentId) { + throw new HTTP400('Missing required path param `attachmentId`'); + } + + if (!Object.values(ATTACHMENT_TYPE).includes(req.body?.attachment_type)) { + throw new HTTP400('Invalid body param `attachment_type`'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + if (req.body.attachment_type === ATTACHMENT_TYPE.REPORT) { + const metadata = new PutReportAttachmentMetadata({ + ...req.body.attachment_meta, + revision_count: req.body.revision_count + }); + + // Update the metadata fields of the attachment record + await updateSurveyReportAttachmentMetadata( + Number(req.params.surveyId), + Number(req.params.attachmentId), + metadata, + connection + ); + + // Delete any existing attachment author records + await deleteSurveyReportAttachmentAuthors(Number(req.params.attachmentId), connection); + + const promises = []; + + // Insert any new attachment author records + promises.push( + metadata.authors.map((author) => + insertSurveyReportAttachmentAuthor(Number(req.params.attachmentId), author, connection) + ) + ); + + await Promise.all(promises); + } + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateProjectAttachmentMetadata', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +const updateSurveyReportAttachmentMetadata = async ( + surveyId: number, + attachmentId: number, + metadata: PutReportAttachmentMetadata, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.updateSurveyReportAttachmentMetadataSQL(surveyId, attachmentId, metadata); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update attachment report statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to update attachment report record'); + } +}; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/delete.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/delete.test.ts index 7c45b0a982..60e8e87080 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/delete.test.ts @@ -1,15 +1,15 @@ +import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as delete_survey from './delete'; -import * as db from '../../../../../database/db'; -import * as survey_attachments_queries from '../../../../../queries/survey/survey-attachments-queries'; -import * as survey_delete_queries from '../../../../../queries/survey/survey-delete-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import survey_queries from '../../../../../queries/survey'; import * as file_utils from '../../../../../utils/file-utils'; -import { DeleteObjectOutput } from 'aws-sdk/clients/s3'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as delete_survey from './delete'; chai.use(sinonChai); @@ -53,8 +53,8 @@ describe('deleteSurvey', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -66,7 +66,7 @@ describe('deleteSurvey', () => { } }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(null); try { const result = delete_survey.deleteSurvey(); @@ -74,8 +74,8 @@ describe('deleteSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -92,7 +92,7 @@ describe('deleteSurvey', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); try { const result = delete_survey.deleteSurvey(); @@ -100,8 +100,8 @@ describe('deleteSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get survey attachments'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get survey attachments'); } }); @@ -118,8 +118,8 @@ describe('deleteSurvey', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveySQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveySQL').returns(null); try { const result = delete_survey.deleteSurvey(); @@ -127,8 +127,8 @@ describe('deleteSurvey', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -145,8 +145,8 @@ describe('deleteSurvey', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveySQL').returns(SQL`something`); sinon.stub(file_utils, 'deleteFileFromS3').resolves(null); const result = delete_survey.deleteSurvey(); @@ -169,8 +169,8 @@ describe('deleteSurvey', () => { query: mockQuery }); - sinon.stub(survey_attachments_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyAttachmentsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveySQL').returns(SQL`something`); sinon.stub(file_utils, 'deleteFileFromS3').resolves('non null response' as DeleteObjectOutput); const result = delete_survey.deleteSurvey(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/delete.ts index 5b2c522be0..e936db458a 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/delete.ts @@ -1,23 +1,36 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { getSurveyAttachmentsSQL } from '../../../../../queries/survey/survey-attachments-queries'; -import { deleteSurveySQL } from '../../../../../queries/survey/survey-delete-queries'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { deleteFileFromS3 } from '../../../../../utils/file-utils'; import { getLogger } from '../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/delete'); -export const DELETE: Operation = [deleteSurvey()]; +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteSurvey() +]; DELETE.apiDoc = { description: 'Delete a survey.', tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -82,7 +95,7 @@ export function deleteSurvey(): RequestHandler { * PART 2 * Delete the survey and all associated records/resources from our DB */ - const deleteSurveySQLStatement = deleteSurveySQL(Number(req.params.surveyId)); + const deleteSurveySQLStatement = queries.survey.deleteSurveySQL(Number(req.params.surveyId)); if (!deleteSurveySQLStatement) { throw new HTTP400('Failed to build SQL delete statement'); @@ -114,7 +127,7 @@ export function deleteSurvey(): RequestHandler { } export const getSurveyAttachmentS3Keys = async (surveyId: number, connection: IDBConnection) => { - const getSurveyAttachmentSQLStatement = getSurveyAttachmentsSQL(surveyId); + const getSurveyAttachmentSQLStatement = queries.survey.getSurveyAttachmentsSQL(surveyId); if (!getSurveyAttachmentSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.test.ts index 315da77081..8e967cdf57 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as observationSubmission from './get'; -import * as db from '../../../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../../../queries/survey/survey-occurrence-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as observationSubmission from './get'; chai.use(sinonChai); @@ -50,8 +51,8 @@ describe('getObservationSubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -63,7 +64,7 @@ describe('getObservationSubmission', () => { } }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(null); try { const result = observationSubmission.getOccurrenceSubmission(); @@ -71,8 +72,10 @@ describe('getObservationSubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL getLatestSurveyOccurrenceSubmissionSQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build SQL getLatestSurveyOccurrenceSubmissionSQL statement' + ); } }); @@ -98,7 +101,7 @@ describe('getObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); const result = observationSubmission.getOccurrenceSubmission(); @@ -134,8 +137,8 @@ describe('getObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); - sinon.stub(survey_occurrence_queries, 'getOccurrenceSubmissionMessagesSQL').returns(null); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getOccurrenceSubmissionMessagesSQL').returns(null); try { const result = observationSubmission.getOccurrenceSubmission(); @@ -143,8 +146,10 @@ describe('getObservationSubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL getOccurrenceSubmissionMessagesSQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build SQL getOccurrenceSubmissionMessagesSQL statement' + ); } }); @@ -191,8 +196,8 @@ describe('getObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); - sinon.stub(survey_occurrence_queries, 'getOccurrenceSubmissionMessagesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getOccurrenceSubmissionMessagesSQL').returns(SQL`something`); const result = observationSubmission.getOccurrenceSubmission(); @@ -234,7 +239,7 @@ describe('getObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`something`); const result = observationSubmission.getOccurrenceSubmission(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.ts index af26686f1e..a65ddc22f0 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/get.ts @@ -1,26 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; -import { - getLatestSurveyOccurrenceSubmissionSQL, - getOccurrenceSubmissionMessagesSQL -} from '../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/submission/get'); -export const GET: Operation = [getOccurrenceSubmission()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getOccurrenceSubmission() +]; GET.apiDoc = { description: 'Fetches an observation occurrence submission for a survey.', tags: ['observation_submission'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -48,6 +57,7 @@ GET.apiDoc = { 'application/json': { schema: { type: 'object', + nullable: true, properties: { id: { type: 'number' @@ -58,6 +68,7 @@ GET.apiDoc = { }, status: { description: 'The validation status of the submission', + nullable: true, type: 'string' }, messages: { @@ -102,7 +113,9 @@ export function getOccurrenceSubmission(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getOccurrenceSubmissionSQLStatement = getLatestSurveyOccurrenceSubmissionSQL(Number(req.params.surveyId)); + const getOccurrenceSubmissionSQLStatement = queries.survey.getLatestSurveyOccurrenceSubmissionSQL( + Number(req.params.surveyId) + ); if (!getOccurrenceSubmissionSQLStatement) { throw new HTTP400('Failed to build SQL getLatestSurveyOccurrenceSubmissionSQL statement'); @@ -132,7 +145,9 @@ export function getOccurrenceSubmission(): RequestHandler { if (errorStatus === 'Rejected' || errorStatus === 'System Error') { const occurrence_submission_id = occurrenceSubmissionData.rows[0].id; - const getSubmissionErrorListSQLStatement = getOccurrenceSubmissionMessagesSQL(Number(occurrence_submission_id)); + const getSubmissionErrorListSQLStatement = queries.survey.getOccurrenceSubmissionMessagesSQL( + Number(occurrence_submission_id) + ); if (!getSubmissionErrorListSQLStatement) { throw new HTTP400('Failed to build SQL getOccurrenceSubmissionMessagesSQL statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.test.ts index 2fec09c98e..91b89c3662 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.test.ts @@ -4,9 +4,10 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../../../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../utils/file-utils'; -import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../__mocks__/db'; import * as upload from './upload'; chai.use(sinonChai); @@ -16,115 +17,144 @@ describe('uploadObservationSubmission', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); + it('should throw a 400 error when files are missing', async () => { + const dbConnectionObj = getMockDBConnection(); - const mockReq = { - keycloak_token: {}, - params: { - projectId: 1, - surveyId: 2 - }, - body: {}, - files: [ - { - fieldname: 'media', - originalname: 'test.txt', - encoding: '7bit', - mimetype: 'text/plain', - size: 340 - } - ] - } as any; - - let actualStatus = 0; - - const mockRes = { - status: (status: number) => { - actualStatus = status; - return { - send: () => { - //do nothing - } - }; - } - } as any; + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - const mockNext = {} as any; + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = []; - it('should throw a 400 error when files are missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, files: [] }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing upload data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); } }); it('should throw a 400 error when more than 1 file uploaded', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'file1' + }, + { + fieldname: 'file2' + } + ] as any; + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, files: ['file1', 'file2'] }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Too many files uploaded, expected 1'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Too many files uploaded, expected 1'); } }); it('should throw a 400 error when projectId is missing', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + const dbConnectionObj = getMockDBConnection(); - try { - const result = upload.uploadMedia(); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - await result({ ...mockReq, params: { ...mockReq.params, projectId: null } }, mockRes, mockNext); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: projectId'); - } - }); + mockReq.params = { + projectId: '', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; - it('should throw a 400 error when surveyId is missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, params: { ...mockReq.params, surveyId: null } }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: surveyId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: projectId'); } }); - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + it('should throw a 400 error when surveyId is missing', async () => { + const dbConnectionObj = getMockDBConnection(); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(null); - sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - const result = upload.uploadMedia(); + mockReq.params = { + projectId: '1', + surveyId: '' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - await result(mockReq, mockRes, mockNext); + const requestHandler = upload.uploadMedia(); + + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to query template methodology species table'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: surveyId'); } }); it('should throw a 400 error when file contains malicious content', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -134,21 +164,39 @@ describe('uploadObservationSubmission', () => { sinon.stub(file_utils, 'scanFileForVirus').resolves(false); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Malicious content detected, upload cancelled'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); } }); it('should throw a 400 error when it fails to insert a record in the database', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); - mockQuery.resolves({ rowCount: 0 }); + mockQuery.onCall(0).resolves({ rowCount: 0 }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -159,20 +207,38 @@ describe('uploadObservationSubmission', () => { }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey occurrence submission record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey occurrence submission record'); } }); it('should throw a 400 error when it fails to get the update SQL', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); @@ -186,25 +252,43 @@ describe('uploadObservationSubmission', () => { }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(null); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey occurrence submission record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); it('should throw a 400 error when it fails to get the update the record in the database', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); - mockQuery.onCall(1).resolves({ rowCount: 0 }); + mockQuery.onCall(1).resolves(null); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -215,24 +299,43 @@ describe('uploadObservationSubmission', () => { }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey occurrence submission record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update survey occurrence submission record'); } }); it('should throw a 400 error when it fails to insert a record in S3', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(1).resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -243,173 +346,48 @@ describe('uploadObservationSubmission', () => { }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'uploadFileToS3').rejects('Failed to insert occurrence submission data'); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.name).to.equal('Failed to insert occurrence submission data'); + expect((actualError as HTTPError).name).to.equal('Failed to insert occurrence submission data'); } }); - it('should return 200 on success with no methodology selected', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( - { ...mockReq, auth_payload: { preferred_username: 'user', email: 'example@email.com' } }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); - - it('should return 200 on success with the `Moose SRB or Composition Survey Skeena` methodology selected', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( - { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Skeena.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); - - it('should return 200 on success with the `Moose SRB or Composition Survey Omineca` methodology selected', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); + it('should return 200 on success', async () => { + const dbConnectionObj = getMockDBConnection(); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Omineca.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); - - it('should return 200 on success with the `Moose SRB or Composition Survey Cariboo` methodology selected', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( - { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Cariboo.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + mockReq['auth_payload'] = { + preferred_username: 'user', + email: 'example@email.com' + }; - it('should return 200 on success with the `Moose SRB or Composition Survey Okanagan` methodology selected', async () => { const mockQuery = sinon.stub(); - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(1).resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(2).resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -419,77 +397,39 @@ describe('uploadObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result( - { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Okanagan.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); + await requestHandler(mockReq, mockRes, mockNext); + expect(mockRes.statusValue).to.equal(200); }); - it('should return 200 on success with the `Moose SRB or Composition Survey Kootenay` methodology selected', async () => { - const mockQuery = sinon.stub(); + it('should throw a 400 error when it fails to get the insertSurveyOccurrenceSubmissionSQL SQL', async () => { + const dbConnectionObj = getMockDBConnection(); - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Kootenay.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; - it('should return 200 on success with the `Moose Recruitment Survey` methodology selected', async () => { const mockQuery = sinon.stub(); - mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); + mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, @@ -499,67 +439,17 @@ describe('uploadObservationSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_occurrence_queries, 'updateSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); - - sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - - const result = upload.uploadMedia(); - - await result( - { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_Recruitment_Survey.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); - expect(actualStatus).to.equal(200); - }); - - it('should throw a 400 error when no sql statement returned for getTemplateMethodologySpeciesIdSQLStatement', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); + sinon.stub(file_utils, 'scanFileForVirus').resolves(true); + sinon.stub(survey_queries, 'insertSurveyOccurrenceSubmissionSQL').returns(null); - sinon.stub(survey_occurrence_queries, 'getTemplateMethodologySpeciesIdSQLStatement').returns(null); + const requestHandler = upload.uploadMedia(); try { - const result = upload.uploadMedia(); - - await result( - { - ...mockReq, - files: [ - { - fieldname: 'media', - originalname: 'Moose_SRB_or_Composition_Survey_Skeena.xlsx', - encoding: '7bit', - mimetype: 'text/csv', - size: 340 - } - ], - auth_payload: { preferred_username: 'user', email: 'example@email.com' } - }, - mockRes, - mockNext - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get template methodology species id sql statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.ts index 90651007ec..cfe15f1a84 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/upload.ts @@ -1,22 +1,27 @@ -'use strict'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; -import { - insertSurveyOccurrenceSubmissionSQL, - updateSurveyOccurrenceSubmissionSQL, - getTemplateMethodologySpeciesIdSQLStatement -} from '../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../utils/logger'; -import { logRequest } from '../../../../../../../utils/path-utils'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/submission/upload'); export const POST: Operation = [ - logRequest('paths/project/{projectId}/survey/{surveyId}/observation/submission/upload', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), uploadMedia() ]; @@ -25,7 +30,7 @@ POST.apiDoc = { tags: ['attachments'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -59,7 +64,19 @@ POST.apiDoc = { }, responses: { 200: { - description: 'Upload OK' + description: 'Upload OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + submissionId: { + type: 'number' + } + } + } + } + } }, 400: { $ref: '#/components/responses/400' @@ -128,16 +145,10 @@ export function uploadMedia(): RequestHandler { throw new HTTP400('Malicious content detected, upload cancelled'); } - const templateMethodologyId = await getTemplateMethodologySpeciesIdStatement( - Number(req.params.surveyId), - connection - ); - const response = await insertSurveyOccurrenceSubmission( Number(req.params.surveyId), 'BioHub', rawMediaFile.originalname, - templateMethodologyId, connection ); @@ -180,7 +191,6 @@ export function uploadMedia(): RequestHandler { * @param {number} surveyId * @param {string} source * @param {string} inputFileName - * @param {(number | null)} templateMethodologyId * @param {IDBConnection} connection * @return {*} {Promise} */ @@ -188,14 +198,12 @@ export const insertSurveyOccurrenceSubmission = async ( surveyId: number, source: string, inputFileName: string, - templateMethodologyId: number | null, connection: IDBConnection ): Promise => { - const insertSqlStatement = insertSurveyOccurrenceSubmissionSQL({ + const insertSqlStatement = queries.survey.insertSurveyOccurrenceSubmissionSQL({ surveyId, source, - inputFileName, - templateMethodologyId + inputFileName }); if (!insertSqlStatement) { @@ -204,38 +212,13 @@ export const insertSurveyOccurrenceSubmission = async ( const insertResponse = await connection.query(insertSqlStatement.text, insertSqlStatement.values); - if (!insertResponse || !insertResponse.rowCount) { + if (!insertResponse.rowCount) { throw new HTTP400('Failed to insert survey occurrence submission record'); } return insertResponse; }; -/** - * Inserts a new record into the `occurrence_submission` table. - * - * @param {number} surveyId - * @param {IDBConnection} connection - * @return {*} {Promise} - */ -export const getTemplateMethodologySpeciesIdStatement = async ( - surveyId: number, - connection: IDBConnection -): Promise => { - const getIdSqlStatement = getTemplateMethodologySpeciesIdSQLStatement(surveyId); - - if (!getIdSqlStatement) { - throw new HTTP400('Failed to build SQL get template methodology species id sql statement'); - } - const getIdResponse = await connection.query(getIdSqlStatement.text, getIdSqlStatement.values); - - if (!getIdResponse) { - throw new HTTP400('Failed to query template methodology species table'); - } - - return getIdResponse?.rows?.[0]?.template_methodology_species_id || null; -}; - /** * Update existing `occurrence_submission` record with inputKey. * @@ -249,7 +232,7 @@ export const updateSurveyOccurrenceSubmissionWithKey = async ( inputKey: string, connection: IDBConnection ): Promise => { - const updateSqlStatement = updateSurveyOccurrenceSubmissionSQL({ submissionId, inputKey }); + const updateSqlStatement = queries.survey.updateSurveyOccurrenceSubmissionSQL({ submissionId, inputKey }); if (!updateSqlStatement) { throw new HTTP400('Failed to build SQL update statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.test.ts index c7d023f5d9..ecafd04d1e 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as delete_submission from './delete'; -import * as db from '../../../../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../../../../queries/survey/survey-occurrence-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../../../../__mocks__/db'; +import * as delete_submission from './delete'; chai.use(sinonChai); @@ -50,8 +51,8 @@ describe('deleteOccurrenceSubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -67,8 +68,8 @@ describe('deleteOccurrenceSubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -84,8 +85,8 @@ describe('deleteOccurrenceSubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `submissionId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `submissionId`'); } }); @@ -97,7 +98,7 @@ describe('deleteOccurrenceSubmission', () => { } }); - sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'deleteOccurrenceSubmissionSQL').returns(null); try { const result = delete_submission.deleteOccurrenceSubmission(); @@ -105,8 +106,8 @@ describe('deleteOccurrenceSubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -123,7 +124,7 @@ describe('deleteOccurrenceSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`); const result = delete_submission.deleteOccurrenceSubmission(); @@ -145,7 +146,7 @@ describe('deleteOccurrenceSubmission', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteOccurrenceSubmissionSQL').returns(SQL`something`); const result = delete_submission.deleteOccurrenceSubmission(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.ts index 678e284681..52b77b1c24 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../../errors/CustomError'; -import { deleteOccurrenceSubmissionSQL } from '../../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/delete'); -export const DELETE: Operation = [deleteOccurrenceSubmission()]; +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteOccurrenceSubmission() +]; DELETE.apiDoc = { description: 'Soft deletes an occurrence submission by ID.', tags: ['observation_submission', 'delete'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -99,7 +111,9 @@ export function deleteOccurrenceSubmission(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const deleteSubmissionSQLStatement = deleteOccurrenceSubmissionSQL(Number(req.params.submissionId)); + const deleteSubmissionSQLStatement = queries.survey.deleteOccurrenceSubmissionSQL( + Number(req.params.submissionId) + ); if (!deleteSubmissionSQLStatement) { throw new HTTP400('Failed to build SQL delete statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.test.ts index 9634069d6d..77d192997d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.test.ts @@ -2,12 +2,13 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_signed_url from './getSignedUrl'; -import * as db from '../../../../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../../../../queries/survey/survey-occurrence-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../../../../../__mocks__/db'; +import * as get_signed_url from './getSignedUrl'; chai.use(sinonChai); @@ -52,8 +53,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -70,8 +71,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -88,8 +89,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `submissionId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `submissionId`'); } }); @@ -101,7 +102,7 @@ describe('getSingleSubmissionURL', () => { } }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); try { const result = get_signed_url.getSingleSubmissionURL(); @@ -109,8 +110,8 @@ describe('getSingleSubmissionURL', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -127,7 +128,7 @@ describe('getSingleSubmissionURL', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves(null); const result = get_signed_url.getSingleSubmissionURL(); @@ -150,7 +151,7 @@ describe('getSingleSubmissionURL', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); const result = get_signed_url.getSingleSubmissionURL(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.ts index e9c3af87a0..4f02afb78c 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl.ts @@ -1,19 +1,32 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../../../../errors/CustomError'; -import { getLogger } from '../../../../../../../../utils/logger'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; -import { getSurveyOccurrenceSubmissionSQL } from '../../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { getS3SignedURL } from '../../../../../../../../utils/file-utils'; +import { getLogger } from '../../../../../../../../utils/logger'; import { attachmentApiDocObject } from '../../../../../../../../utils/shared-api-docs'; const defaultLog = getLogger( '/api/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/getSignedUrl' ); -export const GET: Operation = [getSingleSubmissionURL()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSingleSubmissionURL() +]; GET.apiDoc = { ...attachmentApiDocObject( @@ -45,7 +58,19 @@ GET.apiDoc = { }, required: true } - ] + ], + responses: { + 200: { + description: 'Obsesrvation submission signed URL response.', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } }; export function getSingleSubmissionURL(): RequestHandler { @@ -67,7 +92,7 @@ export function getSingleSubmissionURL(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveyOccurrenceSubmissionSQLStatement = getSurveyOccurrenceSubmissionSQL( + const getSurveyOccurrenceSubmissionSQLStatement = queries.survey.getSurveyOccurrenceSubmissionSQL( Number(req.params.submissionId) ); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.test.ts index 7703896688..d1f0aa0c6a 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.test.ts @@ -5,7 +5,8 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../../../../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../../utils/file-utils'; import { ArchiveFile, MediaFile } from '../../../../../../../../utils/media/media-file'; import * as media_utils from '../../../../../../../../utils/media/media-utils'; @@ -55,8 +56,8 @@ describe('getObservationSubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -72,8 +73,8 @@ describe('getObservationSubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -89,8 +90,8 @@ describe('getObservationSubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `submissionId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `submissionId`'); } }); @@ -102,7 +103,7 @@ describe('getObservationSubmissionCSVForView', () => { } }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(null); try { const result = view.getObservationSubmissionCSVForView(); @@ -110,8 +111,8 @@ describe('getObservationSubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -135,7 +136,7 @@ describe('getObservationSubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves((null as unknown) as GetObjectOutput); @@ -145,8 +146,8 @@ describe('getObservationSubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to retrieve file from S3'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to retrieve file from S3'); } }); @@ -170,7 +171,7 @@ describe('getObservationSubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves({ file: 'myfile' } as GetObjectOutput); sinon.stub(media_utils, 'parseUnknownMedia').returns(null); @@ -181,8 +182,8 @@ describe('getObservationSubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to parse submission, file was empty'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to parse submission, file was empty'); } }); @@ -206,7 +207,7 @@ describe('getObservationSubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves({ file: 'myfile' } as GetObjectOutput); sinon @@ -242,7 +243,7 @@ describe('getObservationSubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyOccurrenceSubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves({ file: 'myfile' } as GetObjectOutput); sinon diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.ts index 0778cdabcb..402f344c91 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view.ts @@ -1,11 +1,10 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; -import { HTTP400, HTTP500 } from '../../../../../../../../errors/CustomError'; -import { getSurveyOccurrenceSubmissionSQL } from '../../../../../../../../queries/survey/survey-occurrence-queries'; +import { HTTP400, HTTP500 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { generateS3FileKey, getFileFromS3 } from '../../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../../utils/logger'; import { DWCArchive } from '../../../../../../../../utils/media/dwc/dwc-archive-file'; @@ -15,14 +14,27 @@ import { XLSXCSV } from '../../../../../../../../utils/media/xlsx/xlsx-file'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/observation/submission/{submissionId}/view'); -export const GET: Operation = [getObservationSubmissionCSVForView()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getObservationSubmissionCSVForView() +]; GET.apiDoc = { description: 'Fetches an observation submission csv details for a survey.', tags: ['observation_submission_csv'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -69,10 +81,19 @@ GET.apiDoc = { type: 'string' }, headers: { - type: 'array' + type: 'array', + items: { + type: 'string' + } }, rows: { - type: 'array' + type: 'array', + items: { + type: 'array', + items: { + nullable: true + } + } } } } @@ -119,7 +140,9 @@ export function getObservationSubmissionCSVForView(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSubmissionSQLStatement = getSurveyOccurrenceSubmissionSQL(Number(req.params.submissionId)); + const getSubmissionSQLStatement = queries.survey.getSurveyOccurrenceSubmissionSQL( + Number(req.params.submissionId) + ); if (!getSubmissionSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/publish.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/publish.test.ts index 363902292c..1b8c2102e7 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/publish.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/publish.test.ts @@ -5,8 +5,8 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../../../../../database/db'; -import * as survey_occurrence_queries from '../../../../../queries/survey/survey-occurrence-queries'; -import * as survey_update_queries from '../../../../../queries/survey/survey-update-queries'; +import { HTTPError } from '../../../../../errors/custom-error'; +import survey_queries from '../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../__mocks__/db'; import * as publish from './publish'; @@ -63,8 +63,8 @@ describe('publishSurveyAndOccurrences', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Missing required path parameter: surveyId'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: surveyId'); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -82,8 +82,8 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: (null as unknown) as any }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Missing request body'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing request body'); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -101,8 +101,8 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: { ...sampleReq.body, publish: undefined } }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Missing publish flag in request body'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing publish flag in request body'); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -114,7 +114,7 @@ describe('publishSurveyAndOccurrences', () => { } }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(null); try { const result = publish.publishSurveyAndOccurrences(); @@ -122,8 +122,10 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: { publish: false } }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to build get survey occurrence submission SQL statement'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build get survey occurrence submission SQL statement' + ); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -139,7 +141,7 @@ describe('publishSurveyAndOccurrences', () => { query: mockQuery }); - sinon.stub(survey_occurrence_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getLatestSurveyOccurrenceSubmissionSQL').returns(SQL`some query`); try { const result = publish.publishSurveyAndOccurrences(); @@ -147,8 +149,8 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: { publish: false } }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to get survey occurrence submissions'); - expect(actualError.status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to get survey occurrence submissions'); + expect((actualError as HTTPError).status).to.equal(500); } }); @@ -162,7 +164,7 @@ describe('publishSurveyAndOccurrences', () => { sinon.stub(publish, 'getSurveyOccurrenceSubmission').resolves({ occurrence_submission_id: 1 }); - sinon.stub(survey_occurrence_queries, 'deleteSurveyOccurrencesSQL').returns(null); + sinon.stub(survey_queries, 'deleteSurveyOccurrencesSQL').returns(null); try { const result = publish.publishSurveyAndOccurrences(); @@ -170,8 +172,8 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: { publish: false } }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to build delete survey occurrences SQL statement'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build delete survey occurrences SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -189,7 +191,7 @@ describe('publishSurveyAndOccurrences', () => { sinon.stub(publish, 'getSurveyOccurrenceSubmission').resolves({ occurrence_submission_id: 1 }); - sinon.stub(survey_occurrence_queries, 'deleteSurveyOccurrencesSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyOccurrencesSQL').returns(SQL`some query`); try { const result = publish.publishSurveyAndOccurrences(); @@ -197,8 +199,8 @@ describe('publishSurveyAndOccurrences', () => { await result({ ...sampleReq, body: { publish: false } }, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to delete survey occurrences'); - expect(actualError.status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to delete survey occurrences'); + expect((actualError as HTTPError).status).to.equal(500); } }); @@ -210,7 +212,7 @@ describe('publishSurveyAndOccurrences', () => { } }); - sinon.stub(survey_update_queries, 'updateSurveyPublishStatusSQL').returns(null); + sinon.stub(survey_queries, 'updateSurveyPublishStatusSQL').returns(null); try { const result = publish.publishSurveyAndOccurrences(); @@ -218,8 +220,8 @@ describe('publishSurveyAndOccurrences', () => { await result(sampleReq, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to build survey publish SQL statement'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build survey publish SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); } }); @@ -235,7 +237,7 @@ describe('publishSurveyAndOccurrences', () => { query: mockQuery }); - sinon.stub(survey_update_queries, 'updateSurveyPublishStatusSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyPublishStatusSQL').returns(SQL`some query`); try { const result = publish.publishSurveyAndOccurrences(); @@ -243,8 +245,8 @@ describe('publishSurveyAndOccurrences', () => { await result(sampleReq, sampleRes, sampleNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to update survey publish status'); - expect(actualError.status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to update survey publish status'); + expect((actualError as HTTPError).status).to.equal(500); } }); @@ -270,7 +272,7 @@ describe('publishSurveyAndOccurrences', () => { } }); - sinon.stub(survey_update_queries, 'updateSurveyPublishStatusSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveyPublishStatusSQL').returns(SQL`some query`); const result = publish.publishSurveyAndOccurrences(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/publish.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/publish.ts index 857aff9c64..46c860c35c 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/publish.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/publish.ts @@ -1,21 +1,27 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../database/db'; -import { HTTP400, HTTP500 } from '../../../../../errors/CustomError'; -import { surveyIdResponseObject } from '../../../../../openapi/schemas/survey'; -import { - deleteSurveyOccurrencesSQL, - getLatestSurveyOccurrenceSubmissionSQL -} from '../../../../../queries/survey/survey-occurrence-queries'; -import { updateSurveyPublishStatusSQL } from '../../../../../queries/survey/survey-update-queries'; +import { HTTP400, HTTP500 } from '../../../../../errors/custom-error'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; -import { logRequest } from '../../../../../utils/path-utils'; const defaultLog = getLogger('paths/project/{projectId}/survey/{surveyId}/publish'); export const PUT: Operation = [ - logRequest('paths/project/{projectId}/survey/{surveyId}/publish', 'PUT'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + publishSurveyAndOccurrences() ]; @@ -24,7 +30,7 @@ PUT.apiDoc = { tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -62,7 +68,14 @@ PUT.apiDoc = { 'application/json': { schema: { // TODO is there any return value? or is it just an HTTP status with no content? - ...(surveyIdResponseObject as object) + title: 'Survey Response Object', + type: 'object', + required: ['id'], + properties: { + id: { + type: 'number' + } + } } } } @@ -119,6 +132,8 @@ export function publishSurveyAndOccurrences(): RequestHandler { await publishSurvey(surveyId, publish, connection); + await connection.commit(); + return res.status(200).send(); } catch (error) { defaultLog.error({ label: 'publishSurveyAndOccurrences', message: 'error', error }); @@ -139,7 +154,7 @@ export function publishSurveyAndOccurrences(): RequestHandler { export const deleteOccurrences = async (surveyId: number, connection: IDBConnection) => { const occurrenceSubmission = await getSurveyOccurrenceSubmission(surveyId, connection); - const sqlStatement = deleteSurveyOccurrencesSQL(occurrenceSubmission.id); + const sqlStatement = queries.survey.deleteSurveyOccurrencesSQL(occurrenceSubmission.id); if (!sqlStatement) { throw new HTTP400('Failed to build delete survey occurrences SQL statement'); @@ -158,7 +173,7 @@ export const deleteOccurrences = async (surveyId: number, connection: IDBConnect * @returns {RequestHandler} */ export const publishSurvey = async (surveyId: number, publish: boolean, connection: IDBConnection) => { - const sqlStatement = updateSurveyPublishStatusSQL(surveyId, publish); + const sqlStatement = queries.survey.updateSurveyPublishStatusSQL(surveyId, publish); if (!sqlStatement) { throw new HTTP400('Failed to build survey publish SQL statement'); @@ -181,7 +196,7 @@ export const publishSurvey = async (surveyId: number, publish: boolean, connecti * @return {*} */ export const getSurveyOccurrenceSubmission = async (surveyId: number, connection: IDBConnection) => { - const sqlStatement = getLatestSurveyOccurrenceSubmissionSQL(surveyId); + const sqlStatement = queries.survey.getLatestSurveyOccurrenceSubmissionSQL(surveyId); if (!sqlStatement) { throw new HTTP400('Failed to build get survey occurrence submission SQL statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.test.ts index 0e48dbae9e..1a68cfe069 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as summarySubmission from './get'; -import * as db from '../../../../../../../database/db'; -import * as survey_summary_queries from '../../../../../../../queries/survey/survey-summary-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as summarySubmission from './get'; chai.use(sinonChai); @@ -50,8 +51,8 @@ describe('getSummarySubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -63,7 +64,7 @@ describe('getSummarySubmission', () => { } }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(null); try { const result = summarySubmission.getSurveySummarySubmission(); @@ -71,8 +72,10 @@ describe('getSummarySubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build getLatestSurveySummarySubmissionSQLStatement statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build getLatestSurveySummarySubmissionSQLStatement statement' + ); } }); @@ -97,7 +100,7 @@ describe('getSummarySubmission', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); const result = summarySubmission.getSurveySummarySubmission(); @@ -123,7 +126,7 @@ describe('getSummarySubmission', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); const result = summarySubmission.getSurveySummarySubmission(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.ts index d78549faf3..dc95781bc8 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/get.ts @@ -1,26 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; -import { - getLatestSurveySummarySubmissionSQL, - getSummarySubmissionMessagesSQL -} from '../../../../../../../queries/survey/survey-summary-queries'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/submission/get'); -export const GET: Operation = [getSurveySummarySubmission()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveySummarySubmission() +]; GET.apiDoc = { description: 'Fetches an summary occurrence submission for a survey.', tags: ['summary_submission'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -48,6 +57,7 @@ GET.apiDoc = { 'application/json': { schema: { type: 'object', + nullable: true, properties: { id: { type: 'number' @@ -98,7 +108,9 @@ export function getSurveySummarySubmission(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveySummarySubmissionSQLStatement = getLatestSurveySummarySubmissionSQL(Number(req.params.surveyId)); + const getSurveySummarySubmissionSQLStatement = queries.survey.getLatestSurveySummarySubmissionSQL( + Number(req.params.surveyId) + ); if (!getSurveySummarySubmissionSQLStatement) { throw new HTTP400('Failed to build getLatestSurveySummarySubmissionSQLStatement statement'); @@ -127,7 +139,7 @@ export function getSurveySummarySubmission(): RequestHandler { if (errorStatus === 'Error') { const summary_submission_id = summarySubmissionData.rows[0].id; - const getSummarySubmissionErrorListSQLStatement = getSummarySubmissionMessagesSQL( + const getSummarySubmissionErrorListSQLStatement = queries.survey.getSummarySubmissionMessagesSQL( Number(summary_submission_id) ); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.test.ts index 38af9e4f76..a47733e26d 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.test.ts @@ -4,9 +4,10 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../../../../../../../database/db'; -import * as survey_summary_queries from '../../../../../../../queries/survey/survey-summary-queries'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../utils/file-utils'; -import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../__mocks__/db'; import * as upload from './upload'; chai.use(sinonChai); @@ -16,193 +17,298 @@ describe('uploadSummarySubmission', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); + it('should throw a 400 error when files are missing', async () => { + const dbConnectionObj = getMockDBConnection(); - const mockReq = { - keycloak_token: {}, - params: { - projectId: 1, - surveyId: 2 - }, - body: {}, - files: [ - { - fieldname: 'media', - originalname: 'test.txt', - encoding: '7bit', - mimetype: 'text/plain', - size: 340 - } - ] - } as any; - - let actualResult: any = null; - - const mockRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - }, - send: (status: number) => { - actualResult = status; - } - } as any; + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - const mockNext = {} as any; + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = []; - it('should throw a 400 error when files are missing', async () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, files: [] }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing upload data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing upload data'); } }); it('should throw a 400 error when more than 1 file uploaded', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'file1' + }, + { + fieldname: 'file2' + } + ] as any; + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, files: ['file1', 'file2'] }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Too many files uploaded, expected 1'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Too many files uploaded, expected 1'); } }); it('should throw a 400 error when projectId is missing', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, params: { ...mockReq.params, projectId: null } }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: projectId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: projectId'); } }); it('should throw a 400 error when surveyId is missing', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result({ ...mockReq, params: { ...mockReq.params, surveyId: null } }, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: surveyId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: surveyId'); } }); it('should throw a 400 error when no sql statement returned', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(null); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(null); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); } }); it('should throw a 400 error when file contains malicious content', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 } - }); + ] as any; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); sinon.stub(file_utils, 'scanFileForVirus').resolves(false); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Malicious content detected, upload cancelled'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Malicious content detected, upload cancelled'); } }); it('should throw a 400 error when it fails to insert a record in the database', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 0 }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to insert survey summary submission record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert survey summary submission record'); } }); it('should throw a 400 error when it fails to get the update SQL', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_summary_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(null); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(null); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); it('should throw a 400 error when it fails to get the update the record in the database', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1, rows: [{ id: 1 }] }); @@ -210,152 +316,224 @@ describe('uploadSummarySubmission', () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_summary_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to update survey summary submission record'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update survey summary submission record'); } }); it('should throw a 400 error when it fails to insert a record in S3', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_summary_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); sinon.stub(file_utils, 'uploadFileToS3').rejects('Failed to insert occurrence submission data'); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); try { - await result(mockReq, mockRes, mockNext); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.name).to.equal('Failed to insert occurrence submission data'); + expect((actualError as HTTPError).name).to.equal('Failed to insert occurrence submission data'); } }); it('should return 200 on success with no methodology selected', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + mockReq['auth_payload'] = { + preferred_username: 'user', + email: 'example@email.com' + }; + const mockQuery = sinon.stub(); - const nextSpy = sinon.spy(); mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); sinon.stub(file_utils, 'scanFileForVirus').resolves(true); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); - sinon.stub(survey_summary_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'updateSurveySummarySubmissionWithKeySQL').returns(SQL`some query`); sinon.stub(file_utils, 'uploadFileToS3').resolves({ key: 'projects/1/surveys/1/test.txt' } as any); - const result = upload.uploadMedia(); + const requestHandler = upload.uploadMedia(); - await result( - { ...mockReq, auth_payload: { preferred_username: 'user', email: 'example@email.com' } }, - mockRes, - nextSpy as any - ); + await requestHandler(mockReq, mockRes, mockNext); - expect(nextSpy).to.have.been.called; + expect(mockNext).to.have.been.called; }); it('should return with a 200 if errors messages exist and they are persisted', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + mockReq['parseError'] = 'some error exists'; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionMessageSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionMessageSQL').returns(SQL`some query`); - const result = upload.persistSummaryParseErrors(); + const requestHandler = upload.persistSummaryParseErrors(); - await result({ ...mockReq, parseError: 'some error exists' }, mockRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.equal(200); + expect(mockRes.statusValue).to.equal(200); }); it('should move on the next step is there are no errors to be persisted', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + const mockQuery = sinon.stub(); - const nextSpy = sinon.spy(); mockQuery.resolves({ rowCount: 1, rows: [{ id: 1 }] }); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); - const result = upload.persistSummaryParseErrors(); + const requestHandler = upload.persistSummaryParseErrors(); - await result(mockReq, mockRes as any, nextSpy); + await requestHandler(mockReq, mockRes, mockNext); - expect(nextSpy).to.have.been.called; + expect(mockNext).to.have.been.called; }); it('should throw an error if there are errors when persisting error messages', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.files = [ + { + fieldname: 'media', + originalname: 'test.txt', + encoding: '7bit', + mimetype: 'text/plain', + size: 340 + } + ] as any; + mockReq['parseError'] = 'some error exists'; + const mockQuery = sinon.stub(); mockQuery.resolves({}); sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, - systemUserId: () => { - return 20; - }, query: mockQuery }); - sinon.stub(survey_summary_queries, 'insertSurveySummarySubmissionMessageSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'insertSurveySummarySubmissionMessageSQL').returns(SQL`some query`); - const result = upload.persistSummaryParseErrors(); + const requestHandler = upload.persistSummaryParseErrors(); try { - await result({ ...mockReq, parseError: 'some error exists' }, mockRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal('Failed to insert summary submission message data'); - expect(actualError.status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert summary submission message data'); + expect((actualError as HTTPError).status).to.equal(400); } }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.ts index 4af70ec688..858d3c511f 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/upload.ts @@ -1,31 +1,35 @@ -'use strict'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; import { PostSummaryDetails } from '../../../../../../../models/summaryresults-create'; -import { - insertSurveySummaryDetailsSQL, - insertSurveySummarySubmissionSQL, - updateSurveySummarySubmissionWithKeySQL, - insertSurveySummarySubmissionMessageSQL -} from '../../../../../../../queries/survey/survey-summary-queries'; +import { generateHeaderErrorMessage, generateRowErrorMessage } from '../../../../../../../paths/dwc/validate'; +import { validateXLSX } from '../../../../../../../paths/xlsx/validate'; +import { queries } from '../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization'; import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../utils/logger'; -import { XLSXCSV } from '../../../../../../../utils/media/xlsx/xlsx-file'; -import { logRequest } from '../../../../../../../utils/path-utils'; -import { prepXLSX } from './../../../../../../../paths/xlsx/validate'; -import { ValidationSchemaParser } from '../../../../../../../utils/media/validation/validation-schema-parser'; -import { validateXLSX } from '../../../../../../../paths/xlsx/validate'; -import { IMediaState } from '../../../../../../../utils/media/media-file'; import { ICsvState } from '../../../../../../../utils/media/csv/csv-file'; -import { generateHeaderErrorMessage, generateRowErrorMessage } from '../../../../../../../paths/dwc/validate'; +import { IMediaState, MediaFile } from '../../../../../../../utils/media/media-file'; +import { parseUnknownMedia } from '../../../../../../../utils/media/media-utils'; +import { ValidationSchemaParser } from '../../../../../../../utils/media/validation/validation-schema-parser'; +import { XLSXCSV } from '../../../../../../../utils/media/xlsx/xlsx-file'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/upload'); export const POST: Operation = [ - logRequest('paths/project/{projectId}/survey/{surveyId}/summary/upload', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), uploadMedia(), prepXLSX(), persistSummaryParseErrors(), @@ -41,7 +45,7 @@ POST.apiDoc = { tags: ['results'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -75,7 +79,19 @@ POST.apiDoc = { }, responses: { 200: { - description: 'Upload OK' + description: 'Upload OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + summarySubmissionId: { + type: 'number' + } + } + } + } + } }, 400: { $ref: '#/components/responses/400' @@ -204,6 +220,39 @@ export function uploadMedia(): RequestHandler { }; } +export function prepXLSX(): RequestHandler { + return async (req, res, next) => { + defaultLog.debug({ label: 'prepXLSX', message: 's3File' }); + + try { + const s3File = req['s3File']; + + const parsedMedia = parseUnknownMedia(s3File); + + if (!parsedMedia) { + req['parseError'] = 'Failed to parse submission, file was empty'; + + return next(); + } + + if (!(parsedMedia instanceof MediaFile)) { + req['parseError'] = 'Failed to parse submission, not a valid XLSX CSV file'; + + return next(); + } + + const xlsxCsv = new XLSXCSV(parsedMedia); + + req['xlsx'] = xlsxCsv; + + next(); + } catch (error) { + defaultLog.error({ label: 'prepXLSX', message: 'error', error }); + throw error; + } + }; +} + /** * Inserts a new record into the `survey_summary_submission` table. * @@ -219,7 +268,7 @@ export const insertSurveySummarySubmission = async ( file_name: string, connection: IDBConnection ): Promise => { - const insertSqlStatement = insertSurveySummarySubmissionSQL(surveyId, source, file_name); + const insertSqlStatement = queries.survey.insertSurveySummarySubmissionSQL(surveyId, source, file_name); if (!insertSqlStatement) { throw new HTTP400('Failed to build SQL insert statement'); @@ -247,7 +296,7 @@ export const updateSurveySummarySubmissionWithKey = async ( key: string, connection: IDBConnection ): Promise => { - const updateSqlStatement = updateSurveySummarySubmissionWithKeySQL(submissionId, key); + const updateSqlStatement = queries.survey.updateSurveySummarySubmissionWithKeySQL(submissionId, key); if (!updateSqlStatement) { throw new HTTP400('Failed to build SQL update statement'); @@ -284,7 +333,7 @@ export function persistSummaryParseErrors(): RequestHandler { await connection.commit(); // archive is not parsable, don't continue to next step and return early - return res.send(200); + return res.status(200).send(); } catch (error) { defaultLog.error({ label: 'persistParseErrors', message: 'error', error }); await connection.rollback(); @@ -521,7 +570,7 @@ export function persistSummaryValidationResults(): RequestHandler { return res.status(200).send(); } catch (error) { - defaultLog.debug({ label: 'persistValidationResults', message: 'error', error }); + defaultLog.error({ label: 'persistValidationResults', message: 'error', error }); await connection.rollback(); throw error; } finally { @@ -643,7 +692,7 @@ export const uploadScrapedSummarySubmission = async ( scrapedSummaryDetail: any, connection: IDBConnection ) => { - const sqlStatement = insertSurveySummaryDetailsSQL(summarySubmissionId, scrapedSummaryDetail); + const sqlStatement = queries.survey.insertSurveySummaryDetailsSQL(summarySubmissionId, scrapedSummaryDetail); if (!sqlStatement) { throw new HTTP400('Failed to build SQL post statement'); @@ -673,7 +722,7 @@ export const insertSummarySubmissionMessage = async ( errorCode: string, connection: IDBConnection ): Promise => { - const sqlStatement = insertSurveySummarySubmissionMessageSQL( + const sqlStatement = queries.survey.insertSurveySummarySubmissionMessageSQL( submissionStatusId, submissionMessageType, message, diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.test.ts index 7a182ed5c2..9b2a036d98 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as delete_submission from './delete'; -import * as db from '../../../../../../../../database/db'; -import * as survey_summary_queries from '../../../../../../../../queries/survey/survey-summary-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../../../../__mocks__/db'; +import * as delete_submission from './delete'; chai.use(sinonChai); @@ -50,8 +51,8 @@ describe('deleteSummarySubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -67,8 +68,8 @@ describe('deleteSummarySubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -84,8 +85,8 @@ describe('deleteSummarySubmission', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `summaryId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `summaryId`'); } }); @@ -97,7 +98,7 @@ describe('deleteSummarySubmission', () => { } }); - sinon.stub(survey_summary_queries, 'deleteSummarySubmissionSQL').returns(null); + sinon.stub(survey_queries, 'deleteSummarySubmissionSQL').returns(null); try { const result = delete_submission.deleteSummarySubmission(); @@ -105,8 +106,8 @@ describe('deleteSummarySubmission', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); @@ -123,7 +124,7 @@ describe('deleteSummarySubmission', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'deleteSummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSummarySubmissionSQL').returns(SQL`something`); const result = delete_submission.deleteSummarySubmission(); @@ -145,7 +146,7 @@ describe('deleteSummarySubmission', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'deleteSummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSummarySubmissionSQL').returns(SQL`something`); const result = delete_submission.deleteSummarySubmission(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.ts index a77c3c7fbd..729560e1c4 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete.ts @@ -1,23 +1,35 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { deleteSummarySubmissionSQL } from '../../../../../../../../queries/survey/survey-summary-queries'; -import { SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; -import { HTTP400 } from '../../../../../../../../errors/CustomError'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../../../../utils/logger'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/delete'); -export const DELETE: Operation = [deleteSummarySubmission()]; +export const DELETE: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + deleteSummarySubmission() +]; DELETE.apiDoc = { description: 'Soft deletes a summary submission by ID.', tags: ['summary_submission', 'delete'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -99,7 +111,7 @@ export function deleteSummarySubmission(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const deleteSubmissionSQLStatement = deleteSummarySubmissionSQL(Number(req.params.summaryId)); + const deleteSubmissionSQLStatement = queries.survey.deleteSummarySubmissionSQL(Number(req.params.summaryId)); if (!deleteSubmissionSQLStatement) { throw new HTTP400('Failed to build SQL delete statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.test.ts index e8d7cb77fd..ff2272da22 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.test.ts @@ -2,12 +2,13 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_signed_url from './getSignedUrl'; -import * as db from '../../../../../../../../database/db'; -import * as survey_summary_queries from '../../../../../../../../queries/survey/survey-summary-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../../../../../__mocks__/db'; +import * as get_signed_url from './getSignedUrl'; chai.use(sinonChai); @@ -52,8 +53,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -70,8 +71,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -88,8 +89,8 @@ describe('getSingleSubmissionURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `summaryId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `summaryId`'); } }); @@ -101,7 +102,7 @@ describe('getSingleSubmissionURL', () => { } }); - sinon.stub(survey_summary_queries, 'getSurveySummarySubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getSurveySummarySubmissionSQL').returns(null); try { const result = get_signed_url.getSingleSummarySubmissionURL(); @@ -109,8 +110,8 @@ describe('getSingleSubmissionURL', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -127,7 +128,7 @@ describe('getSingleSubmissionURL', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveySummarySubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves(null); const result = get_signed_url.getSingleSummarySubmissionURL(); @@ -150,7 +151,7 @@ describe('getSingleSubmissionURL', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getSurveySummarySubmissionSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveySummarySubmissionSQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); const result = get_signed_url.getSingleSummarySubmissionURL(); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.ts index 22eade94bc..05a11d1e33 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl.ts @@ -1,17 +1,30 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../../../../errors/CustomError'; -import { getLogger } from '../../../../../../../../utils/logger'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { getS3SignedURL } from '../../../../../../../../utils/file-utils'; +import { getLogger } from '../../../../../../../../utils/logger'; import { attachmentApiDocObject } from '../../../../../../../../utils/shared-api-docs'; -import { getSurveySummarySubmissionSQL } from '../../../../../../../../queries/survey/survey-summary-queries'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/getSignedUrl'); -export const GET: Operation = [getSingleSummarySubmissionURL()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSingleSummarySubmissionURL() +]; GET.apiDoc = { ...attachmentApiDocObject( @@ -43,7 +56,19 @@ GET.apiDoc = { }, required: true } - ] + ], + responses: { + 200: { + description: 'Submission summary signed URL response.', + content: { + 'application/json': { + schema: { + type: 'string' + } + } + } + } + } }; export function getSingleSummarySubmissionURL(): RequestHandler { @@ -65,7 +90,9 @@ export function getSingleSummarySubmissionURL(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSurveySummarySubmissionSQLStatement = getSurveySummarySubmissionSQL(Number(req.params.summaryId)); + const getSurveySummarySubmissionSQLStatement = queries.survey.getSurveySummarySubmissionSQL( + Number(req.params.summaryId) + ); if (!getSurveySummarySubmissionSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.test.ts index 5f8ce59e29..b04681aca2 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.test.ts @@ -5,7 +5,8 @@ import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; import * as db from '../../../../../../../../database/db'; -import * as survey_summary_queries from '../../../../../../../../queries/survey/survey-summary-queries'; +import { HTTPError } from '../../../../../../../../errors/custom-error'; +import survey_queries from '../../../../../../../../queries/survey'; import * as file_utils from '../../../../../../../../utils/file-utils'; import { MediaFile } from '../../../../../../../../utils/media/media-file'; import * as media_utils from '../../../../../../../../utils/media/media-utils'; @@ -55,8 +56,8 @@ describe('getSurveySubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -72,8 +73,8 @@ describe('getSurveySubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `surveyId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `surveyId`'); } }); @@ -89,8 +90,8 @@ describe('getSurveySubmissionCSVForView', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `summaryId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `summaryId`'); } }); @@ -102,7 +103,7 @@ describe('getSurveySubmissionCSVForView', () => { } }); - sinon.stub(survey_summary_queries, 'getSurveySummarySubmissionSQL').returns(null); + sinon.stub(survey_queries, 'getSurveySummarySubmissionSQL').returns(null); try { const result = view.getSummarySubmissionCSVForView(); @@ -110,8 +111,8 @@ describe('getSurveySubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -135,7 +136,7 @@ describe('getSurveySubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves((null as unknown) as GetObjectOutput); @@ -145,8 +146,8 @@ describe('getSurveySubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to retrieve file from S3'); + expect((actualError as HTTPError).status).to.equal(500); + expect((actualError as HTTPError).message).to.equal('Failed to retrieve file from S3'); } }); @@ -170,7 +171,7 @@ describe('getSurveySubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves({ file: 'myfile' } as GetObjectOutput); sinon.stub(media_utils, 'parseUnknownMedia').returns(null); @@ -181,8 +182,8 @@ describe('getSurveySubmissionCSVForView', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to parse submission, file was empty'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to parse submission, file was empty'); } }); @@ -206,7 +207,7 @@ describe('getSurveySubmissionCSVForView', () => { query: mockQuery }); - sinon.stub(survey_summary_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getLatestSurveySummarySubmissionSQL').returns(SQL`something`); sinon.stub(file_utils, 'generateS3FileKey').resolves('validkey'); sinon.stub(file_utils, 'getFileFromS3').resolves({ file: 'myfile' } as GetObjectOutput); sinon diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.ts index 89eb7e92af..794b6c90e0 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view.ts @@ -1,11 +1,10 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../../../../constants/roles'; import { getDBConnection } from '../../../../../../../../database/db'; -import { HTTP400, HTTP500 } from '../../../../../../../../errors/CustomError'; -import { getSurveySummarySubmissionSQL } from '../../../../../../../../queries/survey/survey-summary-queries'; +import { HTTP400, HTTP500 } from '../../../../../../../../errors/custom-error'; +import { queries } from '../../../../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../../../../request-handlers/security/authorization'; import { generateS3FileKey, getFileFromS3 } from '../../../../../../../../utils/file-utils'; import { getLogger } from '../../../../../../../../utils/logger'; import { DWCArchive } from '../../../../../../../../utils/media/dwc/dwc-archive-file'; @@ -15,14 +14,27 @@ import { XLSXCSV } from '../../../../../../../../utils/media/xlsx/xlsx-file'; const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/submission/{summaryId}/view'); -export const GET: Operation = [getSummarySubmissionCSVForView()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSummarySubmissionCSVForView() +]; GET.apiDoc = { description: 'Fetches a summary submission csv details for a survey.', tags: ['summary_submission_csv'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -69,10 +81,19 @@ GET.apiDoc = { type: 'string' }, headers: { - type: 'array' + type: 'array', + items: { + type: 'string' + } }, rows: { - type: 'array' + type: 'array', + items: { + type: 'array', + items: { + nullable: true + } + } } } } @@ -119,7 +140,7 @@ export function getSummarySubmissionCSVForView(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getSubmissionSQLStatement = getSurveySummarySubmissionSQL(Number(req.params.summaryId)); + const getSubmissionSQLStatement = queries.survey.getSurveySummarySubmissionSQL(Number(req.params.summaryId)); if (!getSubmissionSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.test.ts index 0569562f9d..7602f48867 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.test.ts @@ -2,16 +2,14 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as update from './update'; -import * as create from '../create'; -import * as db from '../../../../../database/db'; -import * as survey_view_update_queries from '../../../../../queries/survey/survey-view-update-queries'; -import * as survey_create_queries from '../../../../../queries/survey/survey-create-queries'; -import * as survey_update_queries from '../../../../../queries/survey/survey-update-queries'; -import * as survey_delete_queries from '../../../../../queries/survey/survey-delete-queries'; import SQL from 'sql-template-strings'; import { COMPLETION_STATUS } from '../../../../../constants/status'; -import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import survey_queries from '../../../../../queries/survey'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../__mocks__/db'; +import * as create from '../create'; +import * as update from './update'; chai.use(sinonChai); @@ -20,32 +18,16 @@ describe('getSurveyForUpdate', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); + it('should throw a 400 error when no survey id path param', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - const sampleReq = { - keycloak_token: {}, - params: { - projectId: 1, - surveyId: 2 - } - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - }, - send: (status: number) => { - actualResult = status; - } - }; - } - }; + mockReq.params = { + projectId: '1', + surveyId: '' + }; - it('should throw a 400 error when no survey id path param', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -54,21 +36,26 @@ describe('getSurveyForUpdate', () => { }); try { - const result = update.getSurveyForUpdate(); + const requestHandler = update.getSurveyForUpdate(); - await result( - { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path parameter: surveyId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: surveyId'); } }); it('should throw a 400 error when no get survey sql statement produced', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -76,29 +63,40 @@ describe('getSurveyForUpdate', () => { } }); - sinon.stub(survey_view_update_queries, 'getSurveyDetailsForUpdateSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyDetailsForUpdateSQL').returns(null); try { - const result = update.getSurveyForUpdate(); + const requestHandler = update.getSurveyForUpdate(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build survey details SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build survey details SQL get statement'); } }); it('should return only survey details when entity specified with survey_details, on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.query = { + entity: ['survey_details'] + }; + const survey_details = { id: 1, name: 'name', - objectives: 'objective', - focal_species: 1, - ancillary_species: 3, - common_survey_methodology_id: 1, - start_date: '2020/04/04', - end_date: '2020/05/05', + focal_species: [1], + ancillary_species: [3], + additional_details: 'details', + start_date: '2020-04-04', + end_date: '2020-05-05', lead_first_name: 'first', lead_last_name: 'last', location_name: 'location', @@ -107,7 +105,7 @@ describe('getSurveyForUpdate', () => { publish_timestamp: null, number: '123', type: 'scientific', - pfs_id: 1 + pfs_id: [1] }; const mockQuery = sinon.stub(); @@ -124,21 +122,20 @@ describe('getSurveyForUpdate', () => { query: mockQuery }); - sinon.stub(survey_view_update_queries, 'getSurveyDetailsForUpdateSQL').returns(SQL`some query`); - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyDetailsForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); - const result = update.getSurveyForUpdate(); + const requestHandler = update.getSurveyForUpdate(); - await result({ ...sampleReq, query: { entity: ['survey_details'] } }, sampleRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.eql({ + expect(mockRes.sendValue).to.eql({ survey_details: { id: 1, survey_name: survey_details.name, - survey_purpose: survey_details.objectives, - focal_species: [survey_details.focal_species], - ancillary_species: [survey_details.ancillary_species], - common_survey_methodology_id: survey_details.common_survey_methodology_id, + focal_species: survey_details.focal_species, + ancillary_species: survey_details.ancillary_species, start_date: survey_details.start_date, end_date: survey_details.end_date, biologist_first_name: survey_details.lead_first_name, @@ -150,13 +147,26 @@ describe('getSurveyForUpdate', () => { permit_type: survey_details.type, completion_status: COMPLETION_STATUS.COMPLETED, publish_date: '', - funding_sources: [1] + funding_sources: survey_details.pfs_id }, + survey_purpose_and_methodology: null, survey_proprietor: null }); }); it('should return survey proprietor info when only survey proprietor entity is specified, on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.query = { + entity: ['survey_proprietor'] + }; + const survey_proprietor = { category_rationale: '', data_sharing_agreement_required: 'false', @@ -184,18 +194,15 @@ describe('getSurveyForUpdate', () => { query: mockQuery }); - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); - const result = update.getSurveyForUpdate(); + const requestHandler = update.getSurveyForUpdate(); - await result( - { ...sampleReq, query: { entity: ['survey_proprietor'] } }, - sampleRes as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.eql({ + expect(mockRes.sendValue).to.eql({ survey_details: null, + survey_purpose_and_methodology: null, survey_proprietor: { category_rationale: survey_proprietor.category_rationale, data_sharing_agreement_required: survey_proprietor.data_sharing_agreement_required, @@ -211,23 +218,41 @@ describe('getSurveyForUpdate', () => { }); }); - it('should return survey details and proprietor info when no entity is specified, on success', async () => { + it('should return survey details, survey purpose and methodology, as well as proprietor info when no entity is specified, on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + const survey_details = { id: 1, name: 'name', - objectives: 'objective', - focal_species: 1, - ancillary_species: 3, - common_survey_methodology_id: 1, - start_date: '2020/04/04', - end_date: '2020/05/05', + focal_species: [1], + ancillary_species: [3], + start_date: '2020-04-04', + end_date: '2020-05-05', lead_first_name: 'first', lead_last_name: 'last', location_name: 'location', revision_count: 1, geometry: [], publish_timestamp: null, - pfs_id: 10 + pfs_id: [10] + }; + + const survey_purpose_and_methodology = { + id: 1, + field_method_id: 1, + additional_details: 'details', + ecological_season_id: 1, + intended_outcome_id: 8, + surveyed_all_areas: true, + vantage_id: 2, + revision_count: 0 }; const survey_proprietor = { @@ -251,6 +276,10 @@ describe('getSurveyForUpdate', () => { rows: [survey_details] }) .onSecondCall() + .resolves({ + rows: [survey_purpose_and_methodology] + }) + .onThirdCall() .resolves({ rows: [survey_proprietor] }); @@ -263,21 +292,20 @@ describe('getSurveyForUpdate', () => { query: mockQuery }); - sinon.stub(survey_view_update_queries, 'getSurveyDetailsForUpdateSQL').returns(SQL`some query`); - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyDetailsForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); - const result = update.getSurveyForUpdate(); + const requestHandler = update.getSurveyForUpdate(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.eql({ + expect(mockRes.sendValue).to.eql({ survey_details: { id: 1, survey_name: survey_details.name, - survey_purpose: survey_details.objectives, - focal_species: [survey_details.focal_species], - ancillary_species: [survey_details.ancillary_species], - common_survey_methodology_id: survey_details.common_survey_methodology_id, + focal_species: survey_details.focal_species, + ancillary_species: survey_details.ancillary_species, start_date: survey_details.start_date, end_date: survey_details.end_date, biologist_first_name: survey_details.lead_first_name, @@ -289,7 +317,17 @@ describe('getSurveyForUpdate', () => { permit_type: '', completion_status: COMPLETION_STATUS.COMPLETED, publish_date: '', - funding_sources: [10] + funding_sources: survey_details.pfs_id + }, + survey_purpose_and_methodology: { + id: 1, + intended_outcome_id: 8, + field_method_id: 1, + additional_details: 'details', + ecological_season_id: 1, + vantage_code_ids: [2], + surveyed_all_areas: 'true', + revision_count: 0 }, survey_proprietor: { category_rationale: survey_proprietor.category_rationale, @@ -312,43 +350,29 @@ describe('updateSurvey', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); + it('should throw a 400 error when no project id path param', async () => { + const dbConnectionObj = getMockDBConnection(); - const sampleReq = { - keycloak_token: {}, - params: { - projectId: 1, - surveyId: 2 - }, - body: { + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '', + surveyId: '2' + }; + mockReq.body = { survey_details: { survey_name: 'name', - survey_purpose: 'purpose', species: 'species', - start_date: '2020/03/03', - end_date: '2020/04/04', + start_date: '2020-03-03', + end_date: '2020-04-04', biologist_first_name: 'first', biologist_last_name: 'last', survey_area_name: 'area name', revision_count: 1, geometry: [] } - } - } as any; - - let actualResult: number = (null as unknown) as number; - - const sampleRes = { - status: (status: number) => { - return { - send: () => { - actualResult = status; - } - }; - } - }; + }; - it('should throw a 400 error when no project id path param', async () => { sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -357,21 +381,39 @@ describe('updateSurvey', () => { }); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path parameter: projectId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: projectId'); } }); it('should throw a 400 error when no survey id path param', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + } + }; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -380,21 +422,27 @@ describe('updateSurvey', () => { }); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, params: { ...sampleReq.params, surveyId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path parameter: surveyId'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: surveyId'); } }); it('should throw a 400 error when no request body present', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = null; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -403,17 +451,39 @@ describe('updateSurvey', () => { }); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result({ ...sampleReq, body: null }, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); } }); it('should throw a 400 error when no revision count', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: null, + geometry: [] + } + }; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -422,21 +492,39 @@ describe('updateSurvey', () => { }); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, body: { ...sampleReq.body, survey_details: { revision_count: null } } }, - (null as unknown) as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to parse request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to parse request body'); } }); it('should throw a 400 error when no sql statement returned (surveyDetails)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + } + }; + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { @@ -444,20 +532,43 @@ describe('updateSurvey', () => { } }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(null); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(null); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); it('should throw a 409 error when no result or rowCount (surveyDetails)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + } + }; + const mockQuery = sinon.stub(); mockQuery.resolves({ rows: null, rowCount: 0 }); @@ -470,20 +581,42 @@ describe('updateSurvey', () => { query: mockQuery }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to update stale survey data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to update stale survey data'); } }); it('should send a valid HTTP response on success (with only survey_details)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + } + }; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); @@ -496,16 +629,31 @@ describe('updateSurvey', () => { query: mockQuery }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.equal(200); + expect(mockRes.statusValue).to.equal(200); }); it('should send a valid HTTP response on success (with only survey proprietor data)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_proprietor: { + survey_data_proprietary: 'false', + id: 0 + } + }; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); @@ -518,20 +666,42 @@ describe('updateSurvey', () => { query: mockQuery }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`some query`); - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, body: { survey_proprietor: { survey_data_proprietary: 'false', id: 0 } } }, - sampleRes as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.equal(200); + expect(mockRes.statusValue).to.equal(200); }); it('should send a valid HTTP response on success (did have proprietor data and no longer requires proprietor data)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + }, + survey_proprietor: { + survey_data_proprietary: 'false', + id: 0 + } + }; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); @@ -544,20 +714,42 @@ describe('updateSurvey', () => { query: mockQuery }); - sinon.stub(survey_delete_queries, 'deleteSurveyProprietorSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'deleteSurveyProprietorSQL').returns(SQL`some query`); - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, body: { ...sampleReq.body, survey_proprietor: { survey_data_proprietary: 'false', id: 0 } } }, - sampleRes as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult).to.equal(200); + expect(mockRes.statusValue).to.equal(200); }); it('should throw HTTP 400 error when no sql statement (did have proprietor data and no longer requires proprietor data)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1', + surveyId: '2' + }; + mockReq.body = { + survey_details: { + survey_name: 'name', + species: 'species', + start_date: '2020-03-03', + end_date: '2020-04-04', + biologist_first_name: 'first', + biologist_last_name: 'last', + survey_area_name: 'area name', + revision_count: 1, + geometry: [] + }, + survey_proprietor: { + survey_data_proprietary: 'false', + id: 1 + } + }; + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); @@ -570,20 +762,16 @@ describe('updateSurvey', () => { query: mockQuery }); - sinon.stub(survey_delete_queries, 'deleteSurveyProprietorSQL').returns(null); + sinon.stub(survey_queries, 'deleteSurveyProprietorSQL').returns(null); try { - const result = update.updateSurvey(); + const requestHandler = update.updateSurvey(); - await result( - { ...sampleReq, body: { ...sampleReq.body, survey_proprietor: { survey_data_proprietary: 'false', id: 1 } } }, - sampleRes as any, - (null as unknown) as any - ); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); } }); }); @@ -593,12 +781,6 @@ describe('updateSurveyProprietorData', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - const surveyId = 2; const entities: update.IUpdateSurvey = { survey_details: { @@ -608,6 +790,7 @@ describe('updateSurveyProprietorData', () => { focal_species: [1], ancillary_species: [2] }, + survey_purpose_and_methodology: {}, survey_proprietor: { id: 0, survey_data_proprietary: 'true' @@ -615,37 +798,43 @@ describe('updateSurveyProprietorData', () => { }; it('should throw a 400 error when fails to build sql statement in case 3', async () => { - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(null); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(null); try { await update.updateSurveyProprietorData(surveyId, entities, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); } }); it('should throw a 400 error when no rowCount in result in case 3', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: null }); - sinon.stub(survey_create_queries, 'postSurveyProprietorSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'postSurveyProprietorSQL').returns(SQL`some`); try { await update.updateSurveyProprietorData(surveyId, entities, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to update survey proprietor data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to update survey proprietor data'); } }); it('should throw a 400 error when fails to build sql statement in case 4', async () => { - sinon.stub(survey_update_queries, 'putSurveyProprietorSQL').returns(null); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(survey_queries, 'putSurveyProprietorSQL').returns(null); try { await update.updateSurveyProprietorData( @@ -656,17 +845,19 @@ describe('updateSurveyProprietorData', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL statement'); } }); it('should throw a 400 error when no rowCount in result in case 4', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: null }); - sinon.stub(survey_update_queries, 'putSurveyProprietorSQL').returns(SQL`some`); + sinon.stub(survey_queries, 'putSurveyProprietorSQL').returns(SQL`some`); try { await update.updateSurveyProprietorData( @@ -677,8 +868,8 @@ describe('updateSurveyProprietorData', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to update survey proprietor data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to update survey proprietor data'); } }); }); @@ -688,12 +879,6 @@ describe('updateSurveyDetailsData', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - const projectId = 1; const surveyId = 2; const data: update.IUpdateSurvey = { @@ -704,10 +889,13 @@ describe('updateSurveyDetailsData', () => { focal_species: [1], ancillary_species: [2] }, + survey_purpose_and_methodology: null, survey_proprietor: null }; it('should throw a 400 error when no revision count in data', async () => { + const dbConnectionObj = getMockDBConnection(); + try { await update.updateSurveyDetailsData( projectId, @@ -718,118 +906,132 @@ describe('updateSurveyDetailsData', () => { expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to parse request body'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to parse request body'); } }); it('should throw a 400 error when no sql statement produced for putSurveyDetailsSQL', async () => { - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(null); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(null); try { await update.updateSurveyDetailsData(projectId, surveyId, data, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); it('should throw a 409 error when no rowCount produced for putSurveyDetailsSQL', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: null }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to update stale survey data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to update stale survey data'); } }); it('should throw a 400 error when no sql produced for deleteFocalSpeciesSQL', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(null); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(null); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); it('should throw a 400 error when no sql produced for deleteAncillarySpeciesSQL', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rowCount: 1 }); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(null); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(null); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL delete statement'); } }); it('should throw a 400 error when fails to delete focal species data', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.onFirstCall().resolves({ rowCount: 1 }).onSecondCall().resolves(null).onThirdCall().resolves(true); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to delete survey focal species data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to delete survey focal species data'); } }); it('should throw a 400 error when fails to delete ancillary species data', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.onFirstCall().resolves({ rowCount: 1 }).onSecondCall().resolves(true).onThirdCall().resolves(null); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to delete survey ancillary species data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to delete survey ancillary species data'); } }); it('should throw a 400 error when fails to delete survey funding sources data', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1 }); @@ -837,22 +1039,24 @@ describe('updateSurveyDetailsData', () => { mockQuery.onCall(2).resolves(true); mockQuery.onCall(3).resolves(null); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); try { await update.updateSurveyDetailsData(projectId, surveyId, data, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to delete survey funding sources data'); + expect((actualError as HTTPError).status).to.equal(409); + expect((actualError as HTTPError).message).to.equal('Failed to delete survey funding sources data'); } }); it('should return resolved promises on success with focal and ancillary species and funding sources but no permit number', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1 }); @@ -860,10 +1064,10 @@ describe('updateSurveyDetailsData', () => { mockQuery.onCall(2).resolves(true); mockQuery.onCall(3).resolves(true); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); sinon.stub(create, 'insertFocalSpecies').resolves(1); sinon.stub(create, 'insertAncillarySpecies').resolves(2); @@ -884,6 +1088,8 @@ describe('updateSurveyDetailsData', () => { }); it('should return resolved promises on success with focal and ancillary species and funding sources and permit number', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.onCall(0).resolves({ rowCount: 1 }); @@ -892,10 +1098,10 @@ describe('updateSurveyDetailsData', () => { mockQuery.onCall(3).resolves(true); mockQuery.onCall(4).resolves(true); - sinon.stub(survey_update_queries, 'putSurveyDetailsSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); - sinon.stub(survey_delete_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'putSurveyDetailsSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteFocalSpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteAncillarySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'deleteSurveyFundingSourcesBySurveyIdSQL').returns(SQL`something`); sinon.stub(create, 'insertFocalSpecies').resolves(1); sinon.stub(create, 'insertAncillarySpecies').resolves(2); @@ -922,41 +1128,39 @@ describe('unassociatePermitFromSurvey', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - const surveyId = 1; it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(survey_update_queries, 'unassociatePermitFromSurveySQL').returns(null); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(survey_queries, 'unassociatePermitFromSurveySQL').returns(null); try { await update.unassociatePermitFromSurvey(surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL update statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL update statement'); } }); it('should throw a 400 error when no result returned', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves(null); - sinon.stub(survey_update_queries, 'unassociatePermitFromSurveySQL').returns(SQL`something`); + sinon.stub(survey_queries, 'unassociatePermitFromSurveySQL').returns(SQL`something`); try { await update.unassociatePermitFromSurvey(surveyId, { ...dbConnectionObj, query: mockQuery }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to update survey permit number data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to update survey permit number data'); } }); }); @@ -966,41 +1170,20 @@ describe('getSurveyDetailsData', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - const surveyId = 1; it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(survey_view_update_queries, 'getSurveyDetailsForUpdateSQL').returns(null); + const dbConnectionObj = getMockDBConnection(); - try { - await update.getSurveyDetailsData(surveyId, dbConnectionObj); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build survey details SQL get statement'); - } - }); - - it('should throw a 400 error when no result returned', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: null }); - - sinon.stub(survey_view_update_queries, 'getSurveyDetailsForUpdateSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyDetailsForUpdateSQL').returns(null); try { - await update.getSurveyDetailsData(surveyId, { ...dbConnectionObj, query: mockQuery }); + await update.getSurveyDetailsData(surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get project survey details data'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build survey details SQL get statement'); } }); }); @@ -1010,33 +1193,31 @@ describe('getSurveyProprietorData', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection({ - systemUserId: () => { - return 20; - } - }); - const surveyId = 1; it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(null); + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(null); try { await update.getSurveyProprietorData(surveyId, dbConnectionObj); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build survey proprietor SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build survey proprietor SQL get statement'); } }); it('should return null when no result returned', async () => { + const dbConnectionObj = getMockDBConnection(); + const mockQuery = sinon.stub(); mockQuery.resolves({ rows: [] }); - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`something`); const result = await update.getSurveyProprietorData(surveyId, { ...dbConnectionObj, query: mockQuery }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts index 8ae2650471..2631834aff 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/update.ts @@ -1,56 +1,68 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; import { getDBConnection, IDBConnection } from '../../../../../database/db'; -import { HTTP400, HTTP409 } from '../../../../../errors/CustomError'; +import { HTTP400, HTTP409, HTTP500 } from '../../../../../errors/custom-error'; +import { PostSurveyProprietorData } from '../../../../../models/survey-create'; import { + GetUpdateSurveyDetailsData, PutSurveyDetailsData, PutSurveyProprietorData, - GetUpdateSurveyDetailsData + PutSurveyPurposeAndMethodologyData } from '../../../../../models/survey-update'; -import { GetSurveyProprietorData } from '../../../../../models/survey-view-update'; -import { - surveyIdResponseObject, - surveyUpdateGetResponseObject, - surveyUpdatePutRequestObject -} from '../../../../../openapi/schemas/survey'; -import { - getSurveyDetailsForUpdateSQL, - getSurveyProprietorForUpdateSQL -} from '../../../../../queries/survey/survey-view-update-queries'; -import { - unassociatePermitFromSurveySQL, - putSurveyDetailsSQL, - putSurveyProprietorSQL -} from '../../../../../queries/survey/survey-update-queries'; -import { - deleteFocalSpeciesSQL, - deleteAncillarySpeciesSQL, - deleteSurveyProprietorSQL, - deleteSurveyFundingSourcesBySurveyIdSQL -} from '../../../../../queries/survey/survey-delete-queries'; +import { GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData } from '../../../../../models/survey-view-update'; +import { geoJsonFeature } from '../../../../../openapi/schemas/geoJson'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; import { getLogger } from '../../../../../utils/logger'; -import { logRequest } from '../../../../../utils/path-utils'; -import { insertAncillarySpecies, insertFocalSpecies, insertSurveyFundingSource, insertSurveyPermit } from '../create'; -import { postSurveyProprietorSQL } from '../../../../../queries/survey/survey-create-queries'; -import { PostSurveyProprietorData } from '../../../../../models/survey-create'; - +import { + insertAncillarySpecies, + insertFocalSpecies, + insertSurveyFundingSource, + insertSurveyPermit, + insertVantageCodes +} from '../create'; export interface IUpdateSurvey { survey_details: object | null; + survey_purpose_and_methodology: object | null; survey_proprietor: object | null; } const defaultLog = getLogger('paths/project/{projectId}/survey/{surveyId}/update'); export const GET: Operation = [ - logRequest('paths/project/{projectId}/survey/{surveyId}/update', 'GET'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), getSurveyForUpdate() ]; -export const PUT: Operation = [logRequest('paths/project/{projectId}/survey/{surveyId}/update', 'PUT'), updateSurvey()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateSurvey() +]; export enum GET_SURVEY_ENTITIES { survey_details = 'survey_details', + survey_purpose_and_methodology = 'survey_purpose_and_methodology', survey_proprietor = 'survey_proprietor' } @@ -61,7 +73,7 @@ GET.apiDoc = { tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -99,7 +111,179 @@ GET.apiDoc = { content: { 'application/json': { schema: { - ...(surveyUpdateGetResponseObject as object) + title: 'Survey get response object, for update purposes', + type: 'object', + required: ['survey_details', 'survey_purpose_and_methodology', 'survey_proprietor'], + properties: { + survey_details: { + description: 'Survey Details', + type: 'object', + nullable: true, + required: [ + 'id', + 'focal_species', + 'ancillary_species', + 'biologist_first_name', + 'biologist_last_name', + 'completion_status', + 'start_date', + 'end_date', + 'funding_sources', + 'geometry', + 'permit_number', + 'permit_type', + 'publish_date', + 'revision_count', + 'survey_area_name', + 'survey_name' + ], + properties: { + id: { + description: 'Survey id', + type: 'number' + }, + ancillary_species: { + type: 'array', + items: { + type: 'number' + } + }, + focal_species: { + type: 'array', + items: { + type: 'number' + } + }, + biologist_first_name: { + type: 'string' + }, + biologist_last_name: { + type: 'string' + }, + completion_status: { + type: 'string' + }, + start_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the survey start date' + }, + end_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'ISO 8601 date string for the survey end date', + nullable: true + }, + funding_sources: { + type: 'array', + items: { + title: 'survey funding agency', + type: 'number' + } + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + }, + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + }, + publish_date: { + type: 'string' + }, + revision_count: { + type: 'number' + }, + survey_area_name: { + type: 'string' + }, + survey_name: { + type: 'string' + } + } + }, + survey_purpose_and_methodology: { + description: 'Survey Details', + type: 'object', + nullable: true, + required: [ + 'field_method_id', + 'intended_outcome_id', + 'ecological_season_id', + 'vantage_code_ids', + 'surveyed_all_areas' + ], + properties: { + id: { + type: 'number' + }, + field_method_id: { + type: 'number' + }, + additional_details: { + type: 'string', + nullable: true + }, + intended_outcome_id: { + type: 'number' + }, + ecological_season_id: { + type: 'number' + }, + vantage_code_ids: { + type: 'array', + items: { + type: 'number' + } + }, + surveyed_all_areas: { + type: 'string', + enum: ['true', 'false'] + }, + revision_count: { + type: 'number' + } + } + }, + survey_proprietor: { + description: 'Survey Details', + type: 'object', + nullable: true, + properties: { + survey_data_proprietary: { + type: 'string' + }, + id: { + type: 'number' + }, + category_rationale: { + type: 'string' + }, + data_sharing_agreement_required: { + type: 'string' + }, + first_nations_id: { + type: 'number', + nullable: true + }, + first_nations_name: { + type: 'string' + }, + proprietary_data_category: { + type: 'number' + }, + proprietary_data_category_name: { + type: 'string' + }, + revision_count: { + type: 'number' + } + } + } + } } } } @@ -127,7 +311,7 @@ PUT.apiDoc = { tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -153,7 +337,176 @@ PUT.apiDoc = { content: { 'application/json': { schema: { - ...(surveyUpdatePutRequestObject as object) + title: 'Survey Put Object', + type: 'object', + properties: { + survey_details: { + description: 'Survey Details', + type: 'object', + required: [ + 'id', + 'focal_species', + 'ancillary_species', + 'biologist_first_name', + 'biologist_last_name', + 'completion_status', + 'start_date', + 'end_date', + 'funding_sources', + 'geometry', + 'permit_number', + 'permit_type', + 'publish_date', + 'revision_count', + 'survey_area_name', + 'survey_name' + ], + properties: { + id: { + description: 'Survey id', + type: 'number' + }, + ancillary_species: { + type: 'array', + items: { + type: 'number' + } + }, + focal_species: { + type: 'array', + items: { + type: 'number' + } + }, + biologist_first_name: { + type: 'string' + }, + biologist_last_name: { + type: 'string' + }, + completion_status: { + type: 'string' + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the survey start date' + }, + end_date: { + type: 'string', + oneOf: [{ maxLength: 0 }, { format: 'date' }], + nullable: true, + description: 'ISO 8601 date string for the survey end date' + }, + funding_sources: { + type: 'array', + items: { + title: 'survey funding agency', + type: 'number' + } + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + }, + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + }, + publish_date: { + type: 'string' + }, + revision_count: { + type: 'number' + }, + survey_area_name: { + type: 'string' + }, + survey_name: { + type: 'string' + } + } + }, + survey_purpose_and_methodology: { + description: 'Survey Details', + type: 'object', + required: [ + 'field_method_id', + 'intended_outcome_id', + 'ecological_season_id', + 'vantage_code_ids', + 'surveyed_all_areas' + ], + properties: { + id: { + type: 'number' + }, + field_method_id: { + type: 'number' + }, + additional_details: { + type: 'string' + }, + intended_outcome_id: { + type: 'number' + }, + ecological_season_id: { + type: 'number' + }, + vantage_code_ids: { + type: 'array', + items: { + type: 'number' + } + }, + surveyed_all_areas: { + type: 'string', + enum: ['true', 'false'] + }, + revision_count: { + type: 'number' + } + } + }, + survey_proprietor: { + description: 'Survey Details', + type: 'object', + //Note: do not make any of these fields required as the object can be null + properties: { + survey_data_proprietary: { + type: 'string' + }, + id: { + type: 'number' + }, + category_rationale: { + type: 'string' + }, + data_sharing_agreement_required: { + type: 'string' + }, + first_nations_id: { + type: 'number' + }, + first_nations_name: { + type: 'string' + }, + proprietary_data_category: { + type: 'number' + }, + proprietary_data_category_name: { + type: 'string' + }, + revision_count: { + type: 'number' + } + } + } + } } } } @@ -164,7 +517,14 @@ PUT.apiDoc = { content: { 'application/json': { schema: { - ...(surveyIdResponseObject as object) + title: 'Survey Response Object', + type: 'object', + required: ['id'], + properties: { + id: { + type: 'number' + } + } } } } @@ -189,6 +549,7 @@ PUT.apiDoc = { export interface IGetSurveyForUpdate { survey_details: GetUpdateSurveyDetailsData | null; + survey_purpose_and_methodology: GetSurveyPurposeAndMethodologyData | null; survey_proprietor: GetSurveyProprietorData | null; } @@ -214,6 +575,7 @@ export function getSurveyForUpdate(): RequestHandler { const results: IGetSurveyForUpdate = { survey_details: null, + survey_purpose_and_methodology: null, survey_proprietor: null }; @@ -227,6 +589,14 @@ export function getSurveyForUpdate(): RequestHandler { ); } + if (entities.includes(GET_SURVEY_ENTITIES.survey_purpose_and_methodology)) { + promises.push( + getSurveyPurposeAndMethodologyData(surveyId, connection).then((value) => { + results.survey_purpose_and_methodology = value; + }) + ); + } + if (entities.includes(GET_SURVEY_ENTITIES.survey_proprietor)) { promises.push( getSurveyProprietorData(surveyId, connection).then((value) => { @@ -253,7 +623,7 @@ export const getSurveyDetailsData = async ( surveyId: number, connection: IDBConnection ): Promise => { - const sqlStatement = getSurveyDetailsForUpdateSQL(surveyId); + const sqlStatement = queries.survey.getSurveyDetailsForUpdateSQL(surveyId); if (!sqlStatement) { throw new HTTP400('Failed to build survey details SQL get statement'); @@ -261,7 +631,7 @@ export const getSurveyDetailsData = async ( const response = await connection.query(sqlStatement.text, sqlStatement.values); - const result = (response && response.rows && new GetUpdateSurveyDetailsData(response.rows)) || null; + const result = (response && response?.rows.length && new GetUpdateSurveyDetailsData(response.rows[0])) || null; if (!result) { throw new HTTP400('Failed to get project survey details data'); @@ -270,11 +640,26 @@ export const getSurveyDetailsData = async ( return result; }; +export const getSurveyPurposeAndMethodologyData = async ( + surveyId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyPurposeAndMethodologyForUpdateSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build survey proprietor SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + return (response && response.rows && new GetSurveyPurposeAndMethodologyData(response.rows)[0]) || null; +}; + export const getSurveyProprietorData = async ( surveyId: number, connection: IDBConnection ): Promise => { - const sqlStatement = getSurveyProprietorForUpdateSQL(surveyId); + const sqlStatement = queries.survey.getSurveyProprietorForUpdateSQL(surveyId); if (!sqlStatement) { throw new HTTP400('Failed to build survey proprietor SQL get statement'); @@ -320,6 +705,10 @@ export function updateSurvey(): RequestHandler { promises.push(updateSurveyDetailsData(projectId, surveyId, entities, connection)); } + if (entities.survey_purpose_and_methodology) { + promises.push(updateSurveyPurposeAndMethodologyData(surveyId, entities, connection)); + } + if (entities.survey_proprietor) { promises.push(updateSurveyProprietorData(surveyId, entities, connection)); } @@ -353,7 +742,12 @@ export const updateSurveyDetailsData = async ( throw new HTTP400('Failed to parse request body'); } - const updateSurveySQLStatement = putSurveyDetailsSQL(projectId, surveyId, putDetailsData, revision_count); + const updateSurveySQLStatement = queries.survey.putSurveyDetailsSQL( + projectId, + surveyId, + putDetailsData, + revision_count + ); if (!updateSurveySQLStatement) { throw new HTTP400('Failed to build SQL update statement'); @@ -365,9 +759,9 @@ export const updateSurveyDetailsData = async ( throw new HTTP409('Failed to update stale survey data'); } - const sqlDeleteFocalSpeciesStatement = deleteFocalSpeciesSQL(surveyId); - const sqlDeleteAncillarySpeciesStatement = deleteAncillarySpeciesSQL(surveyId); - const sqlDeleteSurveyFundingSourcesStatement = deleteSurveyFundingSourcesBySurveyIdSQL(surveyId); + const sqlDeleteFocalSpeciesStatement = queries.survey.deleteFocalSpeciesSQL(surveyId); + const sqlDeleteAncillarySpeciesStatement = queries.survey.deleteAncillarySpeciesSQL(surveyId); + const sqlDeleteSurveyFundingSourcesStatement = queries.survey.deleteSurveyFundingSourcesBySurveyIdSQL(surveyId); if ( !sqlDeleteFocalSpeciesStatement || @@ -432,7 +826,7 @@ export const updateSurveyDetailsData = async ( updating an existing record of the permit table and setting the survey id column value */ promises.push(unassociatePermitFromSurvey(surveyId, connection)); - + // TODO 20211108: currently permit insert vs update is dictated by permit_type (needs fixing/updating) if (putDetailsData.permit_number) { promises.push( insertSurveyPermit(putDetailsData.permit_number, putDetailsData.permit_type, projectId, surveyId, connection) @@ -462,17 +856,20 @@ export const updateSurveyProprietorData = async ( // 2. did have proprietor data; no longer requires proprietor data // delete old record - sqlStatement = deleteSurveyProprietorSQL(surveyId, putProprietorData.id); + sqlStatement = queries.survey.deleteSurveyProprietorSQL(surveyId, putProprietorData.id); } else if (!wasProprietary && isProprietary) { // 3. did not have proprietor data; now requires proprietor data // insert new record - sqlStatement = postSurveyProprietorSQL(surveyId, new PostSurveyProprietorData(entities.survey_proprietor)); + sqlStatement = queries.survey.postSurveyProprietorSQL( + surveyId, + new PostSurveyProprietorData(entities.survey_proprietor) + ); } else { // 4. did have proprietor data; updating proprietor data // update existing record - sqlStatement = putSurveyProprietorSQL(surveyId, putProprietorData); + sqlStatement = queries.survey.putSurveyProprietorSQL(surveyId, putProprietorData); } if (!sqlStatement) { @@ -487,7 +884,7 @@ export const updateSurveyProprietorData = async ( }; export const unassociatePermitFromSurvey = async (survey_id: number, connection: IDBConnection): Promise => { - const sqlStatement = unassociatePermitFromSurveySQL(survey_id); + const sqlStatement = queries.survey.unassociatePermitFromSurveySQL(survey_id); if (!sqlStatement) { throw new HTTP400('Failed to build SQL update statement'); @@ -499,3 +896,68 @@ export const unassociatePermitFromSurvey = async (survey_id: number, connection: throw new HTTP400('Failed to update survey permit number data'); } }; + +export const updateSurveyPurposeAndMethodologyData = async ( + surveyId: number, + entities: IUpdateSurvey, + connection: IDBConnection +): Promise => { + const putPurposeAndMethodologyData = + (entities?.survey_purpose_and_methodology && + new PutSurveyPurposeAndMethodologyData(entities.survey_purpose_and_methodology)) || + null; + + const revision_count = putPurposeAndMethodologyData?.revision_count ?? null; + + if (!revision_count && revision_count !== 0) { + throw new HTTP400('Failed to parse request body'); + } + + const updateSurveySQLStatement = queries.survey.putSurveyPurposeAndMethodologySQL( + surveyId, + putPurposeAndMethodologyData, + revision_count + ); + + if (!updateSurveySQLStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const result = await connection.query(updateSurveySQLStatement.text, updateSurveySQLStatement.values); + if (!result || !result.rowCount) { + throw new HTTP409('Failed to update stale survey data'); + } + + const promises: Promise[] = []; + + promises.push(deleteSurveyVantageCodes(surveyId, connection)); + //Handle vantage codes associated to this survey + + if (putPurposeAndMethodologyData?.vantage_code_ids) { + promises.push( + Promise.all( + putPurposeAndMethodologyData.vantage_code_ids.map((vantageCode: number) => + insertVantageCodes(vantageCode, surveyId, connection) + ) + ) + ); + } + + await Promise.all(promises); + + await connection.commit(); +}; + +export const deleteSurveyVantageCodes = async (survey_id: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.survey.deleteSurveyVantageCodesSQL(survey_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build delete survey vantage codes SQL statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP500('Failed to delete survey vantage codes'); + } +}; diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts index 1b023270e7..12ba608bc2 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.test.ts @@ -1,14 +1,14 @@ import chai, { expect } from 'chai'; -import { COMPLETION_STATUS } from '../../../../../constants/status'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; import SQL from 'sql-template-strings'; +import { COMPLETION_STATUS } from '../../../../../constants/status'; import * as db from '../../../../../database/db'; -import * as survey_view_queries from '../../../../../queries/survey/survey-view-queries'; -import * as survey_view_update_queries from '../../../../../queries/survey/survey-view-update-queries'; -import * as view from './view'; +import { HTTPError } from '../../../../../errors/custom-error'; +import survey_queries from '../../../../../queries/survey'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as view from './view'; chai.use(sinonChai); @@ -31,6 +31,9 @@ describe('getSurveyForView', () => { survey_details: { id: null }, + survey_purpose_and_methodology: { + id: null + }, survey_proprietor: { id: null } @@ -46,76 +49,298 @@ describe('getSurveyForView', () => { } }; - it('should throw a 400 error when no get survey sql statement produced', async () => { + it('should throw a 400 error when no get survey basic data sql statement produced', async () => { + const mockQuery = sinon.fake.resolves({ rowCount: 1, rows: [] }); + sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => { return 20; - } + }, + query: mockQuery }); - sinon.stub(survey_view_queries, 'getSurveyForViewSQL').returns(null); + + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); try { - const result = view.getSurveyForView(); + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no survey basic data returned', async () => { + const mockQuery = sinon.stub(); + + mockQuery + .onCall(0) + .resolves({ + rows: [] // empty response + }) + .onCall(1) + .resolves({ + rows: [{}] + }) + .onCall(2) + .resolves({ + rows: [{}] + }) + .onCall(3) + .resolves({ + rows: [{}] + }) + .onCall(4) + .resolves({ + rows: [{}] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); + + try { + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).message).to.equal('Failed to get survey basic data'); + expect((actualError as HTTPError).status).to.equal(400); } }); - it('should return the survey and survey proprietor row on success', async () => { - const survey_proprietor = { - id: 20, - proprietor_type_id: 12, - proprietor_type_name: 'type', - first_nations_name: 'fn name', - category_rationale: 'rationale', - proprietor_name: 'name', - disa_required: true, - first_nations_id: 1, - survey_data_proprietary: 'true', - revision_count: 3 - }; + it('should throw a 400 error when no get survey funding sql statement produced', async () => { + const mockQuery = sinon.fake.resolves({ rowCount: 1, rows: [] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); + + try { + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no get survey species sql statement produced', async () => { + const mockQuery = sinon.fake.resolves({ rowCount: 1, rows: [] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(null); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); + + try { + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no survey species data returned', async () => { + const mockQuery = sinon.stub(); + + mockQuery + .onCall(0) + .resolves({ + rows: [{}] + }) + .onCall(1) + .resolves({ + rows: [{}] + }) + .onCall(2) + .resolves({ + rows: [{}] + }) + .onCall(3) + .resolves({ + rows: [] // empty response + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); + + try { + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to get species data'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no get survey proprietor sql statement produced', async () => { + const mockQuery = sinon.fake.resolves({ rowCount: 1, rows: [] }); - const survey_details = { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(null); + + try { + const requestHandler = view.getSurveyForView(); + await requestHandler(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should return the survey and survey proprietor row on success', async () => { + const survey_basic_data = { id: 2, - occurrence_submission_id: 3, - summary_results_submission_id: 4, name: 'name', - objectives: 'objective', - focal_species: 'species', - ancillary_species: 'ancillary', - common_survey_methodology: 'method', start_date: '2020/04/04', end_date: '2020/05/05', lead_first_name: 'first', lead_last_name: 'last', location_name: 'location', - revision_count: 1, geometry: [], + survey_basic_data: [], + revision_count: 1, publish_timestamp: null, number: '123', type: 'scientific', + occurrence_submission_id: 3 + }; + const survey_purpose_and_methodology = { + id: 17, + field_method_id: 1, + additional_details: 'details', + ecological_season_id: 1, + intended_outcome_id: 8, + vantage_id: 2, + surveyed_all_areas: false, + revision_count: 0 + }; + + const survey_funding_source_data = { pfs_id: 1, - agency_name: 'agency', + funding_amount: 100, funding_start_date: '2020/04/04', funding_end_date: '2020/05/05', - funding_amount: 100 + agency_name: 'agency' + }; + + const survey_focal_species_data = { + focal_species: [], + focal_species_names: [] + }; + const survey_ancillary_species_data = { + ancillary_species: [], + ancillary_species_names: [] + }; + + const survey_proprietor_data = { + id: 20, + proprietor_type_id: 12, + proprietor_type_name: 'type', + first_nations_name: 'fn name', + first_nations_id: 1, + category_rationale: 'rationale', + proprietor_name: 'name', + disa_required: true, + survey_data_proprietary: 'true', + revision_count: 3 }; const mockQuery = sinon.stub(); mockQuery - .onFirstCall() + .onCall(0) + .resolves({ + rows: [survey_basic_data] + }) + .onCall(1) + .resolves({ + rows: [survey_purpose_and_methodology] + }) + .onCall(2) + .resolves({ + rows: [survey_funding_source_data] + }) + .onCall(3) + .resolves({ + rows: [survey_focal_species_data] + }) + .onCall(4) .resolves({ - rows: [survey_details] + rows: [survey_ancillary_species_data] }) - .onSecondCall() + .onCall(5) .resolves({ - rows: [survey_proprietor] + rows: [survey_proprietor_data] }); sinon.stub(db, 'getDBConnection').returns({ @@ -126,8 +351,12 @@ describe('getSurveyForView', () => { query: mockQuery }); - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); - sinon.stub(survey_view_queries, 'getSurveyForViewSQL').returns(SQL`some query`); + sinon.stub(survey_queries, 'getSurveyBasicDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFundingSourcesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyFocalSpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyAncillarySpeciesDataForViewSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyPurposeAndMethodologyForUpdateSQL').returns(SQL`valid sql`); + sinon.stub(survey_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`valid sql`); const result = view.getSurveyForView(); @@ -135,78 +364,56 @@ describe('getSurveyForView', () => { expect(actualResult.survey_details.id).to.equal(2); expect(actualResult.survey_details).to.eql({ - id: survey_details.id, - occurrence_submission_id: survey_details.occurrence_submission_id, - summary_results_submission_id: survey_details.summary_results_submission_id, - survey_name: survey_details.name, - survey_purpose: survey_details.objectives, - focal_species: [survey_details.focal_species], - ancillary_species: [survey_details.ancillary_species], - common_survey_methodology: survey_details.common_survey_methodology, - start_date: survey_details.start_date, - end_date: survey_details.end_date, - biologist_first_name: survey_details.lead_first_name, - biologist_last_name: survey_details.lead_last_name, - survey_area_name: survey_details.location_name, - revision_count: survey_details.revision_count, - geometry: survey_details.geometry, - permit_number: survey_details.number, - permit_type: survey_details.type, + id: survey_basic_data.id, + occurrence_submission_id: survey_basic_data.occurrence_submission_id, + survey_name: survey_basic_data.name, + focal_species: survey_focal_species_data.focal_species, + focal_species_names: survey_focal_species_data.focal_species_names, + ancillary_species: survey_ancillary_species_data.ancillary_species, + ancillary_species_names: survey_ancillary_species_data.ancillary_species_names, + start_date: survey_basic_data.start_date, + end_date: survey_basic_data.end_date, + biologist_first_name: survey_basic_data.lead_first_name, + biologist_last_name: survey_basic_data.lead_last_name, + survey_area_name: survey_basic_data.location_name, + revision_count: survey_basic_data.revision_count, + geometry: survey_basic_data.geometry, + permit_number: survey_basic_data.number, + permit_type: survey_basic_data.type, completion_status: COMPLETION_STATUS.COMPLETED, publish_date: '', funding_sources: [ { - pfs_id: survey_details.pfs_id, - agency_name: survey_details.agency_name, - funding_start_date: survey_details.funding_start_date, - funding_end_date: survey_details.funding_end_date, - funding_amount: survey_details.funding_amount + pfs_id: survey_funding_source_data.pfs_id, + agency_name: survey_funding_source_data.agency_name, + funding_start_date: survey_funding_source_data.funding_start_date, + funding_end_date: survey_funding_source_data.funding_end_date, + funding_amount: survey_funding_source_data.funding_amount } ] }); expect(actualResult.survey_proprietor).to.eql({ - id: survey_proprietor.id, - proprietary_data_category: survey_proprietor.proprietor_type_id, - proprietary_data_category_name: survey_proprietor.proprietor_type_name, - first_nations_name: survey_proprietor.first_nations_name, - first_nations_id: survey_proprietor.first_nations_id, - category_rationale: survey_proprietor.category_rationale, - proprietor_name: survey_proprietor.proprietor_name, - survey_data_proprietary: survey_proprietor.survey_data_proprietary, + id: survey_proprietor_data.id, + proprietary_data_category: survey_proprietor_data.proprietor_type_id, + proprietary_data_category_name: survey_proprietor_data.proprietor_type_name, + first_nations_name: survey_proprietor_data.first_nations_name, + first_nations_id: survey_proprietor_data.first_nations_id, + category_rationale: survey_proprietor_data.category_rationale, + proprietor_name: survey_proprietor_data.proprietor_name, + survey_data_proprietary: survey_proprietor_data.survey_data_proprietary, data_sharing_agreement_required: 'true', - revision_count: survey_proprietor.revision_count + revision_count: survey_proprietor_data.revision_count }); - }); - - it('should return null when response has no rows (no survey/survey proprietor found)', async () => { - const mockQuery = sinon.stub(); - mockQuery - .onFirstCall() - .resolves({ - rows: null - }) - .onSecondCall() - .resolves({ - rows: null - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + expect(actualResult.survey_purpose_and_methodology).to.eql({ + id: survey_purpose_and_methodology.id, + additional_details: survey_purpose_and_methodology.additional_details, + ecological_season_id: survey_purpose_and_methodology.ecological_season_id, + field_method_id: survey_purpose_and_methodology.field_method_id, + intended_outcome_id: survey_purpose_and_methodology.intended_outcome_id, + vantage_code_ids: [survey_purpose_and_methodology.vantage_id], + surveyed_all_areas: 'false', + revision_count: survey_purpose_and_methodology.revision_count }); - - sinon.stub(survey_view_update_queries, 'getSurveyProprietorForUpdateSQL').returns(SQL`some query`); - sinon.stub(survey_view_queries, 'getSurveyForViewSQL').returns(SQL`some query`); - - const result = view.getSurveyForView(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult.survey_details).to.be.null; - expect(actualResult.survey_proprietor).to.be.null; }); }); diff --git a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts index 0ad71bf06e..a3ddc832a9 100644 --- a/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts +++ b/api/src/paths/project/{projectId}/survey/{surveyId}/view.ts @@ -1,20 +1,34 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../../constants/roles'; -import { getDBConnection } from '../../../../../database/db'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { GetSurveyProprietorData } from '../../../../../models/survey-view-update'; -import { GetViewSurveyDetailsData } from '../../../../../models/survey-view'; -import { surveyViewGetResponseObject } from '../../../../../openapi/schemas/survey'; -import { getSurveyForViewSQL } from '../../../../../queries/survey/survey-view-queries'; -import { getSurveyProprietorForUpdateSQL } from '../../../../../queries/survey/survey-view-update-queries'; +import { PROJECT_ROLE } from '../../../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../../../database/db'; +import { HTTP400 } from '../../../../../errors/custom-error'; +import { + GetAncillarySpeciesData, + GetFocalSpeciesData, + GetViewSurveyDetailsData +} from '../../../../../models/survey-view'; +import { GetSurveyProprietorData, GetSurveyPurposeAndMethodologyData } from '../../../../../models/survey-view-update'; +import { geoJsonFeature } from '../../../../../openapi/schemas/geoJson'; +import { queries } from '../../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization'; +import { TaxonomyService } from '../../../../../services/taxonomy-service'; import { getLogger } from '../../../../../utils/logger'; -import { logRequest } from '../../../../../utils/path-utils'; const defaultLog = getLogger('paths/project/{projectId}/survey/{surveyId}/view'); export const GET: Operation = [ - logRequest('paths/project/{projectId}/survey/{surveyId}/view', 'GET'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), getSurveyForView() ]; @@ -23,7 +37,7 @@ GET.apiDoc = { tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -46,11 +60,236 @@ GET.apiDoc = { ], responses: { 200: { - description: 'Survey with matching surveyId.', + description: 'Survey with matching surveyId and projectId.', content: { 'application/json': { schema: { - ...(surveyViewGetResponseObject as object) + title: 'Survey get response object, for view purposes', + type: 'object', + required: ['survey_details', 'survey_purpose_and_methodology', 'survey_proprietor'], + properties: { + survey_details: { + description: 'Survey Details', + type: 'object', + required: [ + 'id', + 'occurrence_submission_id', + 'focal_species', + 'focal_species_names', + 'ancillary_species', + 'ancillary_species_names', + 'biologist_first_name', + 'biologist_last_name', + 'completion_status', + 'start_date', + 'end_date', + 'funding_sources', + 'geometry', + 'permit_number', + 'permit_type', + 'publish_date', + 'revision_count', + 'survey_area_name', + 'survey_name' + ], + properties: { + id: { + description: 'Survey id', + type: 'number' + }, + ancillary_species: { + type: 'array', + items: { + type: 'number' + } + }, + ancillary_species_names: { + type: 'array', + items: { + type: 'string' + } + }, + focal_species: { + type: 'array', + items: { + type: 'number' + } + }, + focal_species_names: { + type: 'array', + items: { + type: 'string' + } + }, + biologist_first_name: { + type: 'string' + }, + biologist_last_name: { + type: 'string' + }, + completion_status: { + type: 'string' + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding end_date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding end_date' + }, + funding_sources: { + type: 'array', + items: { + title: 'survey funding agency', + type: 'object', + required: ['agency_name', 'funding_amount', 'funding_start_date', 'funding_end_date'], + properties: { + pfs_id: { + type: 'number', + nullable: true + }, + agency_name: { + type: 'string', + nullable: true + }, + funding_amount: { + type: 'number', + nullable: true + }, + funding_start_date: { + type: 'string', + nullable: true, + description: 'ISO 8601 date string' + }, + funding_end_date: { + type: 'string', + nullable: true, + description: 'ISO 8601 date string' + } + } + } + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + }, + occurrence_submission_id: { + description: 'A survey occurrence submission ID', + type: 'number', + nullable: true, + example: 1 + }, + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + }, + publish_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + nullable: true, + description: 'Determines if the record has been published' + }, + revision_count: { + type: 'number' + }, + survey_area_name: { + type: 'string' + }, + survey_name: { + type: 'string' + } + } + }, + survey_purpose_and_methodology: { + description: 'Survey Details', + type: 'object', + required: [ + 'id', + 'field_method_id', + 'additional_details', + 'intended_outcome_id', + 'ecological_season_id', + 'vantage_code_ids', + 'surveyed_all_areas', + 'revision_count' + ], + properties: { + id: { + type: 'number' + }, + field_method_id: { + type: 'number' + }, + additional_details: { + type: 'string', + nullable: true + }, + intended_outcome_id: { + type: 'number', + nullable: true + }, + ecological_season_id: { + type: 'number', + nullable: true + }, + vantage_code_ids: { + type: 'array', + items: { + type: 'number' + } + }, + surveyed_all_areas: { + type: 'string', + enum: ['true', 'false'] + }, + revision_count: { + type: 'number' + } + } + }, + survey_proprietor: { + description: 'Survey Details', + type: 'object', + nullable: true, + properties: { + survey_data_proprietary: { + type: 'string' + }, + id: { + type: 'number' + }, + category_rationale: { + type: 'string' + }, + data_sharing_agreement_required: { + type: 'string' + }, + first_nations_id: { + type: 'number', + nullable: true + }, + first_nations_name: { + type: 'string', + nullable: true + }, + proprietary_data_category: { + type: 'number' + }, + proprietary_data_category_name: { + type: 'string' + }, + revision_count: { + type: 'number' + } + } + } + } } } } @@ -82,34 +321,49 @@ export function getSurveyForView(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); - try { - const getSurveySQLStatement = getSurveyForViewSQL(Number(req.params.surveyId)); - const getSurveyProprietorSQLStatement = getSurveyProprietorForUpdateSQL(Number(req.params.surveyId)); + if (!req.params.surveyId) { + throw new HTTP400('Missing required path param `surveyId`'); + } - if (!getSurveySQLStatement || !getSurveyProprietorSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } + const surveyId = Number(req.params.surveyId); + try { await connection.open(); - const [surveyData, surveyProprietorData] = await Promise.all([ - await connection.query(getSurveySQLStatement.text, getSurveySQLStatement.values), - await connection.query(getSurveyProprietorSQLStatement.text, getSurveyProprietorSQLStatement.values) + const [ + surveyBasicData, + surveyPurposeAndMethodology, + surveyFundingSourcesData, + SurveyFocalSpeciesData, + SurveyAncillarySpeciesData, + surveyProprietorData + ] = await Promise.all([ + getSurveyBasicDataForView(surveyId, connection), + getSurveyPurposeAndMethodologyDataForView(surveyId, connection), + getSurveyFundingSourcesDataForView(surveyId, connection), + getSurveyFocalSpeciesDataForView(surveyId, connection), + getSurveyAncillarySpeciesDataForView(surveyId, connection), + getSurveyProprietorDataForView(surveyId, connection) ]); await connection.commit(); - const getSurveyData = (surveyData && surveyData.rows && new GetViewSurveyDetailsData(surveyData.rows)) || null; + const getSurveyData = new GetViewSurveyDetailsData({ + ...surveyBasicData, + funding_sources: surveyFundingSourcesData, + ...SurveyFocalSpeciesData, + ...SurveyAncillarySpeciesData + }); + + const getSurveyPurposeAndMethodology = + (surveyPurposeAndMethodology && new GetSurveyPurposeAndMethodologyData(surveyPurposeAndMethodology))[0] || null; const getSurveyProprietorData = - (surveyProprietorData && - surveyProprietorData.rows && - surveyProprietorData.rows[0] && - new GetSurveyProprietorData(surveyProprietorData.rows[0])) || - null; + (surveyProprietorData && new GetSurveyProprietorData(surveyProprietorData)) || null; const result = { survey_details: getSurveyData, + survey_purpose_and_methodology: getSurveyPurposeAndMethodology, survey_proprietor: getSurveyProprietorData }; @@ -122,3 +376,117 @@ export function getSurveyForView(): RequestHandler { } }; } + +export const getSurveyBasicDataForView = async (surveyId: number, connection: IDBConnection): Promise => { + const sqlStatement = queries.survey.getSurveyBasicDataForViewSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get survey basic data'); + } + + return (response && response.rows?.[0]) || null; +}; + +export const getSurveyPurposeAndMethodologyDataForView = async ( + surveyId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyPurposeAndMethodologyForUpdateSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get survey purpose and methodology data'); + } + + return (response && response.rows) || []; +}; + +export const getSurveyFundingSourcesDataForView = async ( + surveyId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyFundingSourcesDataForViewSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP400('Failed to get survey funding sources data'); + } + + return (response && response.rows) || []; +}; + +export const getSurveyFocalSpeciesDataForView = async ( + surveyId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyFocalSpeciesDataForViewSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get species data'); + } + + const taxonomyService = new TaxonomyService(); + + const species = await taxonomyService.getSpeciesFromIds(result); + + return new GetFocalSpeciesData(species); +}; + +export const getSurveyAncillarySpeciesDataForView = async ( + surveyId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.survey.getSurveyAncillarySpeciesDataForViewSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get species data'); + } + + const taxonomyService = new TaxonomyService(); + + const species = await taxonomyService.getSpeciesFromIds(result); + + return new GetAncillarySpeciesData(species); +}; + +export const getSurveyProprietorDataForView = async (surveyId: number, connection: IDBConnection) => { + const sqlStatement = queries.survey.getSurveyProprietorForUpdateSQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + return (response && response.rows?.[0]) || null; +}; diff --git a/api/src/paths/project/{projectId}/surveys.test.ts b/api/src/paths/project/{projectId}/surveys.test.ts deleted file mode 100644 index 74321e2631..0000000000 --- a/api/src/paths/project/{projectId}/surveys.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as surveys from './surveys'; -import * as db from '../../../database/db'; -import * as survey_view_queries from '../../../queries/survey/survey-view-queries'; -import SQL from 'sql-template-strings'; -import { COMPLETION_STATUS } from '../../../constants/status'; -import { getMockDBConnection } from '../../../__mocks__/db'; - -chai.use(sinonChai); - -describe('getSurveyList', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - params: { - projectId: 1 - } - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when no project id path param', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = surveys.getSurveyList(); - - await result( - { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(survey_view_queries, 'getSurveyListSQL').returns(null); - - try { - const result = surveys.getSurveyList(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should return the surveys on success (unpublished and active)', async () => { - const survey = { - id: 1, - name: 'name', - species: 'species', - start_date: '2020/04/04', - end_date: '2099/05/05', - publish_timestamp: null - }; - - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [survey] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_view_queries, 'getSurveyListSQL').returns(SQL`some query`); - - const result = surveys.getSurveyList(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql([ - { - id: 1, - name: 'name', - start_date: '2020/04/04', - end_date: '2099/05/05', - species: ['species'], - publish_status: 'Unpublished', - completion_status: COMPLETION_STATUS.ACTIVE - } - ]); - }); - - it('should return the surveys on success (published and completed)', async () => { - const survey = { - id: 1, - name: 'name', - species: 'species', - start_date: '2020/04/04', - end_date: '2020/05/05', - publish_timestamp: '2020/04/04' - }; - - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [survey] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_view_queries, 'getSurveyListSQL').returns(SQL`some query`); - - const result = surveys.getSurveyList(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql([ - { - id: 1, - name: 'name', - start_date: '2020/04/04', - end_date: '2020/05/05', - species: ['species'], - publish_status: 'Published', - completion_status: COMPLETION_STATUS.COMPLETED - } - ]); - }); - - it('should return empty array when response has no rows (no surveys found)', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: null }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(survey_view_queries, 'getSurveyListSQL').returns(SQL`some query`); - - const result = surveys.getSurveyList(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql([]); - }); -}); diff --git a/api/src/paths/project/{projectId}/surveys.ts b/api/src/paths/project/{projectId}/surveys.ts index c6b1d7164d..d8a926db82 100644 --- a/api/src/paths/project/{projectId}/surveys.ts +++ b/api/src/paths/project/{projectId}/surveys.ts @@ -1,26 +1,35 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; +import { PROJECT_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400 } from '../../../errors/CustomError'; -import { GetSurveyListData } from '../../../models/survey-view'; -import { surveyIdResponseObject } from '../../../openapi/schemas/survey'; -import { getSurveyListSQL } from '../../../queries/survey/survey-view-queries'; +import { HTTP400 } from '../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { SurveyService } from '../../../services/survey-service'; import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; -import moment from 'moment'; -import { COMPLETION_STATUS } from '../../../constants/status'; const defaultLog = getLogger('paths/project/{projectId}/surveys'); -export const GET: Operation = [logRequest('paths/project/{projectId}/surveys', 'GET'), getSurveyList()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getSurveyList() +]; GET.apiDoc = { description: 'Get all Surveys.', tags: ['survey'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -41,7 +50,39 @@ GET.apiDoc = { schema: { type: 'array', items: { - ...(surveyIdResponseObject as object) + title: 'Survey Response Object', + type: 'object', + properties: { + survey: { + type: 'object', + properties: { + id: { + type: 'number' + }, + name: { + type: 'string' + }, + publish_status: { + type: 'string' + }, + completion_status: { + type: 'string' + }, + start_date: { + type: 'string', + description: 'ISO 8601 date string' + }, + end_date: { + type: 'string', + description: 'ISO 8601 date string', + nullable: true + } + } + }, + species: { + type: 'object' + } + } } } } @@ -72,37 +113,25 @@ GET.apiDoc = { */ export function getSurveyList(): RequestHandler { return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); if (!req.params.projectId) { throw new HTTP400('Missing required path param `projectId`'); } - const connection = getDBConnection(req['keycloak_token']); - try { - const getSurveyListSQLStatement = getSurveyListSQL(Number(req.params.projectId)); - - if (!getSurveyListSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const getSurveyListResponse = await connection.query( - getSurveyListSQLStatement.text, - getSurveyListSQLStatement.values - ); + const surveyService = new SurveyService(connection); - await connection.commit(); + const surveyIdsResponse = await surveyService.getSurveyIdsByProjectId(Number(req.params.projectId)); - let rows: any[] = []; + const surveyIds = surveyIdsResponse.map((item: { id: any }) => item.id); - if (getSurveyListResponse && getSurveyListResponse.rows && new GetSurveyListData(getSurveyListResponse.rows)) { - rows = new GetSurveyListData(getSurveyListResponse.rows).surveys; - } + const surveys = await surveyService.getSurveysByIds(surveyIds); - const result: any[] = _extractSurveys(rows); + await connection.commit(); - return res.status(200).json(result); + return res.status(200).json(surveys); } catch (error) { defaultLog.error({ label: 'getSurveyList', message: 'error', error }); throw error; @@ -111,36 +140,3 @@ export function getSurveyList(): RequestHandler { } }; } - -/** - * Extract an array of survey data from DB query. - * - * @export - * @param {any[]} rows DB query result rows - * @return {any[]} An array of survey data - */ -export function _extractSurveys(rows: any[]): any[] { - if (!rows || !rows.length) { - return []; - } - - const surveys: any[] = []; - - rows.forEach((row) => { - const survey: any = { - id: row.id, - name: row.name, - species: row.species, - start_date: row.start_date, - end_date: row.end_date, - publish_status: row.publish_timestamp ? 'Published' : 'Unpublished', - completion_status: - (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || - COMPLETION_STATUS.ACTIVE - }; - - surveys.push(survey); - }); - - return surveys; -} diff --git a/api/src/paths/project/{projectId}/update.test.ts b/api/src/paths/project/{projectId}/update.test.ts index f49cf5e9ac..d25017f84a 100644 --- a/api/src/paths/project/{projectId}/update.test.ts +++ b/api/src/paths/project/{projectId}/update.test.ts @@ -2,102 +2,169 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as update from './update'; import * as db from '../../../database/db'; -import { IUpdateProject } from './update'; -import * as project_delete_queries from '../../../queries/project/project-delete-queries'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../../__mocks__/db'; +import { HTTPError } from '../../../errors/custom-error'; +import { GetPermitData } from '../../../models/project-view'; +import { ProjectService } from '../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import * as update from './update'; chai.use(sinonChai); -describe('updateProjectPermitData', () => { - afterEach(() => { - sinon.restore(); - }); +describe('update', () => { + describe('getProjectForUpdate', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '' + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update.getProjectForUpdate(); - const dbConnectionObj = getMockDBConnection(); - - const projectId = 1; - const entities = { - permit: { - permits: [ - { - permit_number: 1, - permit_type: 'type' - } - ] - }, - coordinator: { - first_name: 'first', - last_name: 'last', - email_address: 'email@example.com', - coordinator_agency: 'agency', - share_contact_details: 'true', - revision_count: 1 - } - } as IUpdateProject; - - it('should throw a 400 error when no permit entities', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: projectId'); } }); - try { - await update.updateProjectPermitData(projectId, { ...entities, permit: null }, dbConnectionObj); + it('should return selected entities', async () => { + const dbConnectionObj = getMockDBConnection(); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing request body entity `permit`'); - } + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + const sampleResponse = { + id: 1, + coordinator: null, + permit: new GetPermitData(), + project: null, + objectives: null, + location: null, + iucn: null, + funding: null, + partnerships: null + }; + + mockReq.params = { + projectId: '1' + }; + + mockReq.query = { + entity: ['permit'] + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'getProjectEntitiesById').resolves(sampleResponse); + + const requestHandler = update.getProjectForUpdate(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + expect(ProjectService.prototype.getProjectEntitiesById).called.calledWith(1, ['permit']); + expect(mockRes.sendValue).to.equal(sampleResponse); + }); }); - it('should throw a 400 error when failed to generate delete permit SQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; + describe('updateProject', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '' + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = update.updateProject(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path parameter: projectId'); } }); - sinon.stub(project_delete_queries, 'deletePermitSQL').returns(null); + it('should throw a 400 error when no request body', async () => { + const dbConnectionObj = getMockDBConnection(); - try { - await update.updateProjectPermitData(projectId, entities, dbConnectionObj); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); - } - }); + mockReq.params = { + projectId: '1' + }; + + mockReq.body = null; - it('should throw a 409 error when failed to delete permit', async () => { - const mockQuery = sinon.stub(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - mockQuery.resolves(null); + try { + const requestHandler = update.updateProject(); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required request body'); + } }); - sinon.stub(project_delete_queries, 'deletePermitSQL').returns(SQL`something`); + it('updates a project with all valid entries and returns 200 on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + projectId: '1' + }; + + mockReq.body = { + project: { + project_id: 1, + project_name: 'Project 1', + start_date: '2022-02-02', + end_date: '2022-02-30', + objectives: 'my objectives', + publish_date: '2022-02-02', + revision_count: 0 + }, + iucn: {}, + contact: {}, + permit: {}, + funding: {}, + partnerships: {}, + location: {} + }; - try { - await update.updateProjectPermitData(projectId, entities, dbConnectionObj); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(409); - expect(actualError.message).to.equal('Failed to delete project permit data'); - } + sinon.stub(ProjectService.prototype, 'updateProject').resolves(); + + const requestHandler = update.updateProject(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); }); }); diff --git a/api/src/paths/project/{projectId}/update.ts b/api/src/paths/project/{projectId}/update.ts index a4ed3c3cfd..73283eb9b4 100644 --- a/api/src/paths/project/{projectId}/update.ts +++ b/api/src/paths/project/{projectId}/update.ts @@ -1,70 +1,30 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; -import { getDBConnection, IDBConnection } from '../../../database/db'; -import { HTTP400, HTTP409 } from '../../../errors/CustomError'; -import { - GetCoordinatorData, - GetIUCNClassificationData, - GetPartnershipsData, - GetObjectivesData, - GetProjectData, - PutCoordinatorData, - PutPartnershipsData, - PutLocationData, - PutObjectivesData, - PutProjectData, - PutIUCNData, - IGetPutIUCN, - GetLocationData, - PutFundingSource, - GetPermitData -} from '../../../models/project-update'; -import { GetFundingData } from '../../../models/project-view-update'; -import { - projectIdResponseObject, - projectUpdateGetResponseObject, - projectUpdatePutRequestObject -} from '../../../openapi/schemas/project'; -import { - getCoordinatorByProjectSQL, - getIndigenousPartnershipsByProjectSQL, - getIUCNActionClassificationByProjectSQL, - getObjectivesByProjectSQL, - getPermitsByProjectSQL, - getProjectByProjectSQL, - putProjectFundingSourceSQL, - putProjectSQL -} from '../../../queries/project/project-update-queries'; -import { - deleteActivitiesSQL, - deleteIUCNSQL, - deleteIndigenousPartnershipsSQL, - deleteStakeholderPartnershipsSQL, - deleteProjectFundingSourceSQL, - deletePermitSQL -} from '../../../queries/project/project-delete-queries'; -import { - getStakeholderPartnershipsByProjectSQL, - getLocationByProjectSQL, - getActivitiesByProjectSQL -} from '../../../queries/project/project-view-update-queries'; +import { PROJECT_ROLE } from '../../../constants/roles'; +import { getDBConnection } from '../../../database/db'; +import { HTTP400 } from '../../../errors/custom-error'; +import { geoJsonFeature } from '../../../openapi/schemas/geoJson'; +import { projectIdResponseObject, projectUpdatePutRequestObject } from '../../../openapi/schemas/project'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../services/project-service'; import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; -import { - insertClassificationDetail, - insertIndigenousNation, - insertProjectActivity, - insertStakeholderPartnership, - insertPermit, - associateExistingPermitToProject -} from '../../project'; -import { IPostExistingPermit, IPostPermit, PostPermitData } from '../../../models/project-create'; -import { deleteSurveyFundingSourceByProjectFundingSourceIdSQL } from '../../../queries/survey/survey-delete-queries'; -const defaultLog = getLogger('paths/project/{projectId}'); +const defaultLog = getLogger('paths/project/{projectId}/update'); -export const GET: Operation = [logRequest('paths/project/{projectId}/update', 'GET'), getProjectForUpdate()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + getProjectForUpdate() +]; export enum GET_ENTITIES { coordinator = 'coordinator', @@ -84,7 +44,7 @@ GET.apiDoc = { tags: ['project'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -114,8 +74,239 @@ GET.apiDoc = { content: { 'application/json': { schema: { - // TODO this is currently empty, and needs updating - ...(projectUpdateGetResponseObject as object) + title: 'Project get response object, for update purposes', + type: 'object', + required: ['project', 'permit', 'coordinator', 'objectives', 'location', 'iucn', 'funding', 'partnerships'], + properties: { + project: { + description: 'Basic project metadata', + type: 'object', + required: [ + 'project_name', + 'project_type', + 'project_activities', + 'start_date', + 'end_date', + 'publish_date', + 'revision_count' + ], + nullable: true, + properties: { + project_name: { + type: 'string' + }, + project_type: { + type: 'number' + }, + project_activities: { + type: 'array', + items: { + type: 'number' + } + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project end date' + }, + publish_date: { + description: 'Status of the project being published/unpublished', + format: 'date', + type: 'string' + }, + revision_count: { + type: 'number' + } + } + }, + permit: { + type: 'object', + required: ['permits'], + nullable: true, + properties: { + permits: { + type: 'array', + items: { + title: 'Project permit', + type: 'object', + properties: { + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + } + } + } + } + } + }, + coordinator: { + title: 'Project coordinator', + type: 'object', + nullable: true, + required: [ + 'first_name', + 'last_name', + 'email_address', + 'coordinator_agency', + 'share_contact_details', + 'revision_count' + ], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + }, + email_address: { + type: 'string' + }, + coordinator_agency: { + type: 'string' + }, + share_contact_details: { + type: 'string', + enum: ['true', 'false'] + }, + revision_count: { + type: 'number' + } + } + }, + objectives: { + description: 'The project objectives and caveats', + type: 'object', + required: ['objectives', 'caveats'], + nullable: true, + properties: { + objectives: { + type: 'string' + }, + caveats: { + type: 'string' + } + } + }, + location: { + description: 'The project location object', + type: 'object', + required: ['location_description', 'geometry'], + nullable: true, + properties: { + location_description: { + type: 'string' + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + } + } + }, + iucn: { + description: 'The International Union for Conservation of Nature number', + type: 'object', + required: ['classificationDetails'], + nullable: true, + properties: { + classificationDetails: { + type: 'array', + items: { + type: 'object', + properties: { + classification: { + type: 'number' + }, + subClassification1: { + type: 'number' + }, + subClassification2: { + type: 'number' + } + } + } + } + } + }, + funding: { + description: 'The project funding details', + type: 'object', + required: ['fundingSources'], + nullable: true, + properties: { + fundingSources: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + agency_id: { + type: 'number' + }, + investment_action_category: { + type: 'number' + }, + investment_action_category_name: { + type: 'string' + }, + agency_name: { + type: 'string' + }, + funding_amount: { + type: 'number' + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding end_date' + }, + agency_project_id: { + type: 'string' + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + partnerships: { + description: 'The project partners', + type: 'object', + required: ['indigenous_partnerships', 'stakeholder_partnerships'], + nullable: true, + properties: { + indigenous_partnerships: { + type: 'array', + items: { + type: 'number' + } + }, + stakeholder_partnerships: { + type: 'array', + items: { + type: 'string' + } + } + } + } + } } } } @@ -138,23 +329,12 @@ GET.apiDoc = { } }; -export interface IGetProjectForUpdate { - coordinator: GetCoordinatorData | null; - permit: any; - project: any; - objectives: GetObjectivesData | null; - location: any; - iucn: GetIUCNClassificationData | null; - funding: GetFundingData | null; - partnerships: GetPartnershipsData | null; -} - /** * Get a project, for update purposes. * * @returns {RequestHandler} */ -function getProjectForUpdate(): RequestHandler { +export function getProjectForUpdate(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); @@ -169,83 +349,9 @@ function getProjectForUpdate(): RequestHandler { await connection.open(); - const results: IGetProjectForUpdate = { - coordinator: null, - permit: null, - project: null, - objectives: null, - location: null, - iucn: null, - funding: null, - partnerships: null - }; - - const promises: Promise[] = []; - - if (entities.includes(GET_ENTITIES.coordinator)) { - promises.push( - getProjectCoordinatorData(projectId, connection).then((value) => { - results.coordinator = value; - }) - ); - } - - if (entities.includes(GET_ENTITIES.permit)) { - promises.push( - getPermitData(projectId, connection).then((value) => { - results.permit = value; - }) - ); - } - - if (entities.includes(GET_ENTITIES.partnerships)) { - promises.push( - getPartnershipsData(projectId, connection).then((value) => { - results.partnerships = value; - }) - ); - } - - if (entities.includes(GET_ENTITIES.location)) { - promises.push( - getLocationData(projectId, connection).then((value) => { - results.location = value; - }) - ); - } - - if (entities.includes(GET_ENTITIES.iucn)) { - promises.push( - getIUCNClassificationData(projectId, connection).then((value) => { - results.iucn = value; - }) - ); - } + const projectService = new ProjectService(connection); - if (entities.includes(GET_ENTITIES.objectives)) { - promises.push( - getObjectivesData(projectId, connection).then((value) => { - results.objectives = value; - }) - ); - } - - if (entities.includes(GET_ENTITIES.project)) { - promises.push( - getProjectData(projectId, connection).then((value) => { - results.project = value; - }) - ); - } - if (entities.includes(GET_ENTITIES.funding)) { - promises.push( - getProjectData(projectId, connection).then((value) => { - results.project = value; - }) - ); - } - - await Promise.all(promises); + const results = await projectService.getProjectEntitiesById(projectId, entities); await connection.commit(); @@ -259,159 +365,27 @@ function getProjectForUpdate(): RequestHandler { }; } -export const getPermitData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatement = getPermitsByProjectSQL(projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows) || null; - - if (!result) { - throw new HTTP400('Failed to get project permit data'); - } - - return new GetPermitData(result); -}; - -export const getLocationData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatement = getLocationByProjectSQL(projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows) || null; - - if (!result) { - throw new HTTP400('Failed to get project location data'); - } - - return new GetLocationData(result); -}; - -export const getIUCNClassificationData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatement = getIUCNActionClassificationByProjectSQL(projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows) || null; - - if (!result) { - throw new HTTP400('Failed to get project IUCN data'); - } - - return new GetIUCNClassificationData(result); -}; - -export const getProjectCoordinatorData = async ( - projectId: number, - connection: IDBConnection -): Promise => { - const sqlStatement = getCoordinatorByProjectSQL(projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result) { - throw new HTTP400('Failed to get project coordinator data'); - } - - return new GetCoordinatorData(result); -}; - -export const getPartnershipsData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatementIndigenous = getIndigenousPartnershipsByProjectSQL(projectId); - const sqlStatementStakeholder = getStakeholderPartnershipsByProjectSQL(projectId); - - if (!sqlStatementIndigenous || !sqlStatementStakeholder) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const responseIndigenous = await connection.query(sqlStatementIndigenous.text, sqlStatementIndigenous.values); - const responseStakeholder = await connection.query(sqlStatementStakeholder.text, sqlStatementStakeholder.values); - - const resultIndigenous = (responseIndigenous && responseIndigenous.rows) || null; - const resultStakeholder = (responseStakeholder && responseStakeholder.rows) || null; - - if (!resultIndigenous) { - throw new HTTP400('Failed to get indigenous partnership data'); - } - - if (!resultStakeholder) { - throw new HTTP400('Failed to get stakeholder partnership data'); - } - - return new GetPartnershipsData(resultIndigenous, resultStakeholder); -}; - -export const getObjectivesData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatement = getObjectivesByProjectSQL(projectId); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result) { - throw new HTTP400('Failed to get project objectives data'); - } - - return new GetObjectivesData(result); -}; - -export const getProjectData = async (projectId: number, connection: IDBConnection): Promise => { - const sqlStatementDetails = getProjectByProjectSQL(projectId); - const sqlStatementActivities = getActivitiesByProjectSQL(projectId); - - if (!sqlStatementDetails || !sqlStatementActivities) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const [responseDetails, responseActivities] = await Promise.all([ - connection.query(sqlStatementDetails.text, sqlStatementDetails.values), - connection.query(sqlStatementActivities.text, sqlStatementActivities.values) - ]); - - const resultDetails = (responseDetails && responseDetails.rows && responseDetails.rows[0]) || null; - const resultActivities = (responseActivities && responseActivities.rows && responseActivities.rows) || null; - - if (!resultDetails) { - throw new HTTP400('Failed to get project details data'); - } - - if (!resultActivities) { - throw new HTTP400('Failed to get project activities data'); - } - - return new GetProjectData(resultDetails, resultActivities); -}; - -export const PUT: Operation = [logRequest('paths/project/{projectId}/update', 'PUT'), updateProject()]; +export const PUT: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + updateProject() +]; PUT.apiDoc = { description: 'Update a project.', tags: ['project'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -471,7 +445,7 @@ export interface IUpdateProject { * * @returns {RequestHandler} */ -function updateProject(): RequestHandler { +export function updateProject(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); @@ -490,33 +464,13 @@ function updateProject(): RequestHandler { await connection.open(); - const promises: Promise[] = []; - - if (entities?.partnerships) { - promises.push(updateProjectPartnershipsData(projectId, entities, connection)); - } - - if (entities?.project || entities?.location || entities?.objectives || entities?.coordinator) { - promises.push(updateProjectData(projectId, entities, connection)); - } - - if (entities?.permit && entities?.coordinator) { - promises.push(updateProjectPermitData(projectId, entities, connection)); - } - - if (entities?.iucn) { - promises.push(updateProjectIUCNData(projectId, entities, connection)); - } - - if (entities?.funding) { - promises.push(updateProjectFundingData(projectId, entities, connection)); - } + const projectService = new ProjectService(connection); - await Promise.all(promises); + await projectService.updateProject(projectId, entities); await connection.commit(); - return res.status(200).send(); + return res.status(200).json({ id: projectId }); } catch (error) { defaultLog.error({ label: 'updateProject', message: 'error', error }); await connection.rollback(); @@ -526,225 +480,3 @@ function updateProject(): RequestHandler { } }; } - -export const updateProjectPermitData = async ( - projectId: number, - entities: IUpdateProject, - connection: IDBConnection -): Promise => { - if (!entities.permit) { - throw new HTTP400('Missing request body entity `permit`'); - } - - const putPermitData = new PostPermitData(entities.permit); - - const sqlDeleteStatement = deletePermitSQL(projectId); - - if (!sqlDeleteStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - const deleteResult = await connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); - - if (!deleteResult) { - throw new HTTP409('Failed to delete project permit data'); - } - - const insertPermitPromises = - putPermitData?.permits?.map((permit: IPostPermit) => { - return insertPermit(permit.permit_number, permit.permit_type, projectId, connection); - }) || []; - - // Handle existing non-sampling permits which are now being associated to a project - const updateExistingPermitPromises = - putPermitData?.existing_permits?.map((existing_permit: IPostExistingPermit) => { - return associateExistingPermitToProject(existing_permit.permit_id, projectId, connection); - }) || []; - - await Promise.all([insertPermitPromises, updateExistingPermitPromises]); -}; - -export const updateProjectIUCNData = async ( - projectId: number, - entities: IUpdateProject, - connection: IDBConnection -): Promise => { - const putIUCNData = (entities?.iucn && new PutIUCNData(entities.iucn)) || null; - - const sqlDeleteStatement = deleteIUCNSQL(projectId); - - if (!sqlDeleteStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - const deleteResult = await connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); - - if (!deleteResult) { - throw new HTTP409('Failed to delete project IUCN data'); - } - - const insertIUCNPromises = - putIUCNData?.classificationDetails?.map((iucnClassification: IGetPutIUCN) => - insertClassificationDetail(iucnClassification.subClassification2, projectId, connection) - ) || []; - - await Promise.all(insertIUCNPromises); -}; - -export const updateProjectPartnershipsData = async ( - projectId: number, - entities: IUpdateProject, - connection: IDBConnection -): Promise => { - const putPartnershipsData = (entities?.partnerships && new PutPartnershipsData(entities.partnerships)) || null; - - const sqlDeleteIndigenousPartnershipsStatement = deleteIndigenousPartnershipsSQL(projectId); - const sqlDeleteStakeholderPartnershipsStatement = deleteStakeholderPartnershipsSQL(projectId); - - if (!sqlDeleteIndigenousPartnershipsStatement || !sqlDeleteStakeholderPartnershipsStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - const deleteIndigenousPartnershipsPromises = connection.query( - sqlDeleteIndigenousPartnershipsStatement.text, - sqlDeleteIndigenousPartnershipsStatement.values - ); - - const deleteStakeholderPartnershipsPromises = connection.query( - sqlDeleteStakeholderPartnershipsStatement.text, - sqlDeleteStakeholderPartnershipsStatement.values - ); - - const [deleteIndigenousPartnershipsResult, deleteStakeholderPartnershipsResult] = await Promise.all([ - deleteIndigenousPartnershipsPromises, - deleteStakeholderPartnershipsPromises - ]); - - if (!deleteIndigenousPartnershipsResult) { - throw new HTTP409('Failed to delete project indigenous partnerships data'); - } - - if (!deleteStakeholderPartnershipsResult) { - throw new HTTP409('Failed to delete project stakeholder partnerships data'); - } - - const insertIndigenousPartnershipsPromises = - putPartnershipsData?.indigenous_partnerships?.map((indigenousPartnership: number) => - insertIndigenousNation(indigenousPartnership, projectId, connection) - ) || []; - - const insertStakeholderPartnershipsPromises = - putPartnershipsData?.stakeholder_partnerships?.map((stakeholderPartnership: string) => - insertStakeholderPartnership(stakeholderPartnership, projectId, connection) - ) || []; - - await Promise.all([...insertIndigenousPartnershipsPromises, ...insertStakeholderPartnershipsPromises]); -}; - -export const updateProjectData = async ( - projectId: number, - entities: IUpdateProject, - connection: IDBConnection -): Promise => { - const putProjectData = (entities?.project && new PutProjectData(entities.project)) || null; - const putLocationData = (entities?.location && new PutLocationData(entities.location)) || null; - const putObjectivesData = (entities?.objectives && new PutObjectivesData(entities.objectives)) || null; - const putCoordinatorData = (entities?.coordinator && new PutCoordinatorData(entities.coordinator)) || null; - - // Update project table - const revision_count = - putProjectData?.revision_count ?? - putLocationData?.revision_count ?? - putObjectivesData?.revision_count ?? - putCoordinatorData?.revision_count ?? - null; - - if (!revision_count && revision_count !== 0) { - throw new HTTP400('Failed to parse request body'); - } - - const sqlUpdateProject = putProjectSQL( - projectId, - putProjectData, - putLocationData, - putObjectivesData, - putCoordinatorData, - revision_count - ); - - if (!sqlUpdateProject) { - throw new HTTP400('Failed to build SQL update statement'); - } - - const result = await connection.query(sqlUpdateProject.text, sqlUpdateProject.values); - - if (!result || !result.rowCount) { - // TODO if revision count is bad, it is supposed to raise an exception? - // It currently does skip the update as expected, but it just returns 0 rows updated, and doesn't result in any errors - throw new HTTP409('Failed to update stale project data'); - } - - const sqlDeleteActivities = deleteActivitiesSQL(projectId); - - if (!sqlDeleteActivities) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - const deleteActivitiesResult = await connection.query(sqlDeleteActivities.text, sqlDeleteActivities.values); - - if (!deleteActivitiesResult) { - throw new HTTP409('Failed to update project activity data'); - } - - const insertActivityPromises = - putProjectData?.project_activities?.map((activityId: number) => - insertProjectActivity(activityId, projectId, connection) - ) || []; - - await Promise.all([...insertActivityPromises]); -}; - -export const updateProjectFundingData = async ( - projectId: number, - entities: IUpdateProject, - connection: IDBConnection -): Promise => { - const putFundingSource = entities?.funding && new PutFundingSource(entities.funding); - - const surveyFundingSourceDeleteStatement = deleteSurveyFundingSourceByProjectFundingSourceIdSQL(putFundingSource?.id); - const projectFundingSourceDeleteStatement = deleteProjectFundingSourceSQL(projectId, putFundingSource?.id); - - if (!projectFundingSourceDeleteStatement || !surveyFundingSourceDeleteStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - const surveyFundingSourceDeleteResult = await connection.query( - surveyFundingSourceDeleteStatement.text, - surveyFundingSourceDeleteStatement.values - ); - - if (!surveyFundingSourceDeleteResult) { - throw new HTTP409('Failed to delete survey funding source'); - } - - const projectFundingSourceDeleteResult = await connection.query( - projectFundingSourceDeleteStatement.text, - projectFundingSourceDeleteStatement.values - ); - - if (!projectFundingSourceDeleteResult) { - throw new HTTP409('Failed to delete project funding source'); - } - - const sqlInsertStatement = putProjectFundingSourceSQL(putFundingSource, projectId); - - if (!sqlInsertStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const insertResult = await connection.query(sqlInsertStatement.text, sqlInsertStatement.values); - - if (!insertResult) { - throw new HTTP409('Failed to put (insert) project funding source with incremented revision count'); - } -}; diff --git a/api/src/paths/project/{projectId}/view.test.ts b/api/src/paths/project/{projectId}/view.test.ts index 2d4ca44030..fd1c223348 100644 --- a/api/src/paths/project/{projectId}/view.test.ts +++ b/api/src/paths/project/{projectId}/view.test.ts @@ -1,431 +1,72 @@ +import Ajv from 'ajv'; import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as view from './view'; import * as db from '../../../database/db'; -import * as project_view_queries from '../../../queries/project/project-view-queries'; -import * as project_view_update_queries from '../../../queries/project/project-view-update-queries'; -import { getMockDBConnection } from '../../../__mocks__/db'; -import SQL from 'sql-template-strings'; -import { - GetCoordinatorData, - GetIUCNClassificationData, - GetObjectivesData, - GetPartnershipsData, - GetProjectData, - GetLocationData, - GetPermitData -} from '../../../models/project-view'; -import { GetFundingData } from '../../../models/project-view-update'; +import { HTTPError } from '../../../errors/custom-error'; +import { ProjectService } from '../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { GET, viewProject } from './view'; chai.use(sinonChai); -describe('getProjectForView', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - params: { - projectId: 1 - } - } as any; - - let actualResult = { - id: null - }; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when no sql statement returned for getProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(null); - - try { - const result = view.getProjectForView(); +describe('project/{projectId}/view', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 400 error when no sql statement returned for getProjectPermitsSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(null); - - try { - const result = view.getProjectForView(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } }); - it('should throw a 400 error when no sql statement returned for getLocationByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } + describe('viewProject', () => { + afterEach(() => { + sinon.restore(); }); - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(null); + it('fetches a project', async () => { + const dbConnectionObj = getMockDBConnection(); - try { - const result = view.getProjectForView(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 400 error when no sql statement returned for getActivitiesByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); + const viewProjectResult = { id: 1 }; - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(null); + sinon.stub(ProjectService.prototype, 'getProjectById').resolves(viewProjectResult as any); - try { - const result = view.getProjectForView(); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); + try { + const requestHandler = viewProject(); - it('should throw a 400 error when no sql statement returned for getIUCNActionClassificationByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); } - }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(null); - - try { - const result = view.getProjectForView(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 400 error when no sql statement returned for getFundingSourceByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql(viewProjectResult); }); - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(null); + it('catches and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ release: sinon.stub() }); - try { - const result = view.getProjectForView(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - it('should throw a 400 error when no sql statement returned for getIndigenousPartnershipsByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); + sinon.stub(ProjectService.prototype, 'getProjectById').rejects(new Error('a test error')); - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIndigenousPartnershipsByProjectSQL').returns(null); + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - try { - const result = view.getProjectForView(); + try { + const requestHandler = viewProject(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.release).to.have.been.called; - it('should throw a 400 error when no sql statement returned for getStakeholderPartnershipsByProjectSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; + expect((actualError as HTTPError).message).to.equal('a test error'); } }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIndigenousPartnershipsByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getStakeholderPartnershipsByProjectSQL').returns(null); - - try { - const result = view.getProjectForView(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should return id and nulls when all fields return null', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: null - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIndigenousPartnershipsByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getStakeholderPartnershipsByProjectSQL').returns(SQL`some`); - - const result = view.getProjectForView(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql({ - id: 1, - project: null, - permit: null, - coordinator: null, - objectives: null, - location: null, - iucn: null, - funding: null, - partnerships: null - }); - }); - - it('should return proper result for project info on success', async () => { - const mockQuery = sinon.stub(); - - const projectData = { - id: 1, - type: 'type', - name: 'name', - objectives: 'project objectives', - location_description: 'location description', - start_date: '2020/04/04', - end_date: '2020/05/05', - caveats: 'caveats', - comments: 'comment', - coordinator_first_name: 'first', - coordinator_last_name: 'last', - coordinator_email_address: 'coord@email.com', - coordinator_agency_name: 'agency 1', - coordinator_public: true, - geometry: null, - create_date: '2020/04/04', - create_user: null, - update_date: null, - update_user: null, - revision_count: 1, - publish_date: null - }; - - const permitData = { - number: 10, - type: 'Scientific' - }; - - const locationData = { - location_description: 'location description', - geometry: null, - revision_count: 1 - }; - - const activityData = { - activity_id: 19 - }; - - const iucnData = { - classification: 'class', - subClassification1: 'subclass 1', - subClassification2: 'subclass 2' - }; - - const fundingSourceData = { - id: 1, - agency_id: 2, - funding_amount: 20, - start_date: '2020/04/04', - end_date: '2020/05/05', - investment_action_category: 2, - investment_action_category_name: 'iac name', - agency_name: 'agency name', - agency_project_id: 1, - revision_count: 1 - }; - - const indigenousData = { - fn_name: 'fn name' - }; - - const stakeholderData = { - sp_name: 'sp name' - }; - - // getProjectSQL mock - mockQuery.onCall(0).resolves({ - rows: [projectData] - }); - - // getProjectPermitsSQL mock - mockQuery.onCall(1).resolves({ - rows: [permitData] - }); - - // getLocationByProjectSQL mock - mockQuery.onCall(2).resolves({ - rows: [locationData] - }); - - // getActivitiesByProjectSQL mock - mockQuery.onCall(3).resolves({ - rows: [activityData] - }); - - // getIUCNActionClassificationByProjectSQL mock - mockQuery.onCall(4).resolves({ - rows: [iucnData] - }); - - // getFundingSourceByProjectSQL mock - mockQuery.onCall(5).resolves({ - rows: [fundingSourceData] - }); - - // getIndigenousPartnershipsByProjectSQL mock - mockQuery.onCall(6).resolves({ - rows: [indigenousData] - }); - - // getStakeholderPartnershipsByProjectSQL mock - mockQuery.onCall(7).resolves({ - rows: [stakeholderData] - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(project_view_queries, 'getProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getProjectPermitsSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getLocationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getActivitiesByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIUCNActionClassificationByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getFundingSourceByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_queries, 'getIndigenousPartnershipsByProjectSQL').returns(SQL`some`); - sinon.stub(project_view_update_queries, 'getStakeholderPartnershipsByProjectSQL').returns(SQL`some`); - - const result = view.getProjectForView(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql({ - id: 1, - project: new GetProjectData(projectData, [activityData]), - permit: new GetPermitData([permitData]), - coordinator: new GetCoordinatorData(projectData), - objectives: new GetObjectivesData(projectData), - location: new GetLocationData([locationData]), - iucn: new GetIUCNClassificationData([iucnData]), - funding: new GetFundingData([fundingSourceData]), - partnerships: new GetPartnershipsData([indigenousData], [stakeholderData]) - }); }); }); diff --git a/api/src/paths/project/{projectId}/view.ts b/api/src/paths/project/{projectId}/view.ts index 82d4a682bd..54ae6159fe 100644 --- a/api/src/paths/project/{projectId}/view.ts +++ b/api/src/paths/project/{projectId}/view.ts @@ -1,44 +1,35 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; +import { PROJECT_ROLE } from '../../../constants/roles'; import { getDBConnection } from '../../../database/db'; -import { HTTP400 } from '../../../errors/CustomError'; -import { - GetCoordinatorData, - GetIUCNClassificationData, - GetObjectivesData, - GetPartnershipsData, - GetProjectData, - GetLocationData, - GetPermitData -} from '../../../models/project-view'; -import { GetFundingData } from '../../../models/project-view-update'; -import { projectViewGetResponseObject } from '../../../openapi/schemas/project'; -import { - getIndigenousPartnershipsByProjectSQL, - getIUCNActionClassificationByProjectSQL, - getProjectSQL, - getProjectPermitsSQL -} from '../../../queries/project/project-view-queries'; -import { - getStakeholderPartnershipsByProjectSQL, - getLocationByProjectSQL, - getActivitiesByProjectSQL, - getFundingSourceByProjectSQL -} from '../../../queries/project/project-view-update-queries'; +import { geoJsonFeature } from '../../../openapi/schemas/geoJson'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { ProjectService } from '../../../services/project-service'; import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; const defaultLog = getLogger('paths/project/{projectId}/view'); -export const GET: Operation = [logRequest('paths/project/{projectId}/view', 'GET'), getProjectForView()]; +export const GET: Operation = [ + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR, PROJECT_ROLE.PROJECT_VIEWER], + projectId: Number(req.params.projectId), + discriminator: 'ProjectRole' + } + ] + }; + }), + viewProject() +]; GET.apiDoc = { description: 'Get a project, for view-only purposes.', tags: ['project'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -57,7 +48,242 @@ GET.apiDoc = { content: { 'application/json': { schema: { - ...(projectViewGetResponseObject as object) + title: 'Project get response object, for view purposes', + type: 'object', + required: [ + 'id', + 'project', + 'permit', + 'coordinator', + 'objectives', + 'location', + 'iucn', + 'funding', + 'partnerships' + ], + properties: { + id: { + description: 'Project id', + type: 'number' + }, + project: { + description: 'Basic project metadata', + type: 'object', + required: [ + 'project_name', + 'project_type', + 'project_activities', + 'start_date', + 'end_date', + 'comments', + 'completion_status', + 'publish_date' + ], + properties: { + project_name: { + type: 'string' + }, + project_type: { + type: 'number' + }, + project_activities: { + type: 'array', + items: { + type: 'number' + } + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project end date' + }, + comments: { + type: 'string', + description: 'Comments' + }, + completion_status: { + description: 'Status of the project being active/completed', + type: 'string' + }, + publish_date: { + description: 'Status of the project being published/unpublished', + format: 'date', + type: 'string' + } + } + }, + permit: { + type: 'object', + required: ['permits'], + properties: { + permits: { + type: 'array', + items: { + title: 'Project permit', + type: 'object', + properties: { + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + } + } + } + } + } + }, + coordinator: { + title: 'Project coordinator', + type: 'object', + required: ['first_name', 'last_name', 'email_address', 'coordinator_agency', 'share_contact_details'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + }, + email_address: { + type: 'string' + }, + coordinator_agency: { + type: 'string' + }, + share_contact_details: { + type: 'string', + enum: ['true', 'false'] + } + } + }, + objectives: { + description: 'The project objectives and caveats', + type: 'object', + required: ['objectives', 'caveats'], + properties: { + objectives: { + type: 'string' + }, + caveats: { + type: 'string' + } + } + }, + location: { + description: 'The project location object', + type: 'object', + required: ['location_description', 'geometry'], + properties: { + location_description: { + type: 'string' + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + } + } + }, + iucn: { + description: 'The International Union for Conservation of Nature number', + type: 'object', + required: ['classificationDetails'], + properties: { + classificationDetails: { + type: 'array', + items: { + type: 'object', + properties: { + classification: { + type: 'number' + }, + subClassification1: { + type: 'number' + }, + subClassification2: { + type: 'number' + } + } + } + } + } + }, + funding: { + description: 'The project funding details', + type: 'object', + required: ['fundingSources'], + properties: { + fundingSources: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + agency_id: { + type: 'number' + }, + investment_action_category: { + type: 'number' + }, + investment_action_category_name: { + type: 'string' + }, + agency_name: { + type: 'string' + }, + funding_amount: { + type: 'number' + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding end_date' + }, + agency_project_id: { + type: 'string', + nullable: true + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + partnerships: { + description: 'The project partners', + type: 'object', + required: ['indigenous_partnerships', 'stakeholder_partnerships'], + properties: { + indigenous_partnerships: { + type: 'array', + items: { + type: 'number' + } + }, + stakeholder_partnerships: { + type: 'array', + items: { + type: 'string' + } + } + } + } + } } } } @@ -85,116 +311,18 @@ GET.apiDoc = { * * @returns {RequestHandler} */ -export function getProjectForView(): RequestHandler { +export function viewProject(): RequestHandler { return async (req, res) => { const connection = getDBConnection(req['keycloak_token']); try { - const getProjectSQLStatement = getProjectSQL(Number(req.params.projectId)); - const getProjectPermitsSQLStatement = getProjectPermitsSQL(Number(req.params.projectId)); - const getProjectLocationSQLStatement = getLocationByProjectSQL(Number(req.params.projectId)); - const getProjectActivitiesSQLStatement = getActivitiesByProjectSQL(Number(req.params.projectId)); - const getProjectIUCNActionClassificationSQLStatement = getIUCNActionClassificationByProjectSQL( - Number(req.params.projectId) - ); - const getProjectFundingSourceSQLStatement = getFundingSourceByProjectSQL(Number(req.params.projectId)); - const getProjectIndigenousPartnershipsSQLStatement = getIndigenousPartnershipsByProjectSQL( - Number(req.params.projectId) - ); - const getProjectStakeholderPartnershipsSQLStatement = getStakeholderPartnershipsByProjectSQL( - Number(req.params.projectId) - ); - - if ( - !getProjectSQLStatement || - !getProjectPermitsSQLStatement || - !getProjectLocationSQLStatement || - !getProjectActivitiesSQLStatement || - !getProjectIUCNActionClassificationSQLStatement || - !getProjectFundingSourceSQLStatement || - !getProjectIndigenousPartnershipsSQLStatement || - !getProjectStakeholderPartnershipsSQLStatement - ) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const [ - projectData, - permitData, - locationData, - activityData, - iucnClassificationData, - fundingData, - indigenousPartnerships, - stakeholderPartnerships - ] = await Promise.all([ - await connection.query(getProjectSQLStatement.text, getProjectSQLStatement.values), - await connection.query(getProjectPermitsSQLStatement.text, getProjectPermitsSQLStatement.values), - await connection.query(getProjectLocationSQLStatement.text, getProjectLocationSQLStatement.values), - await connection.query(getProjectActivitiesSQLStatement.text, getProjectActivitiesSQLStatement.values), - await connection.query( - getProjectIUCNActionClassificationSQLStatement.text, - getProjectIUCNActionClassificationSQLStatement.values - ), - await connection.query(getProjectFundingSourceSQLStatement.text, getProjectFundingSourceSQLStatement.values), - await connection.query( - getProjectIndigenousPartnershipsSQLStatement.text, - getProjectIndigenousPartnershipsSQLStatement.values - ), - await connection.query( - getProjectStakeholderPartnershipsSQLStatement.text, - getProjectStakeholderPartnershipsSQLStatement.values - ) - ]); - - await connection.commit(); - - const getProjectData = - (projectData && - projectData.rows && - activityData && - activityData.rows && - new GetProjectData(projectData.rows[0], activityData.rows)) || - null; + const projectService = new ProjectService(connection); - const getPermitData = (permitData && permitData.rows && new GetPermitData(permitData.rows)) || null; + const result = await projectService.getProjectById(Number(req.params.projectId)); - const getObjectivesData = (projectData && projectData.rows && new GetObjectivesData(projectData.rows[0])) || null; - - const getLocationData = (locationData && locationData.rows && new GetLocationData(locationData.rows)) || null; - - const getCoordinatorData = - (projectData && projectData.rows && new GetCoordinatorData(projectData.rows[0])) || null; - - const getPartnershipsData = - (indigenousPartnerships && - indigenousPartnerships.rows && - stakeholderPartnerships && - stakeholderPartnerships.rows && - new GetPartnershipsData(indigenousPartnerships.rows, stakeholderPartnerships.rows)) || - null; - - const getIUCNClassificationData = - (iucnClassificationData && - iucnClassificationData.rows && - new GetIUCNClassificationData(iucnClassificationData.rows)) || - null; - - const getFundingData = (fundingData && fundingData.rows && new GetFundingData(fundingData.rows)) || null; - - const result = { - id: req.params.projectId, - project: getProjectData, - permit: getPermitData, - coordinator: getCoordinatorData, - objectives: getObjectivesData, - location: getLocationData, - iucn: getIUCNClassificationData, - funding: getFundingData, - partnerships: getPartnershipsData - }; + await connection.commit(); return res.status(200).json(result); } catch (error) { diff --git a/api/src/paths/projects.test.ts b/api/src/paths/projects.test.ts deleted file mode 100644 index afd8d5a885..0000000000 --- a/api/src/paths/projects.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { expect } from 'chai'; -//import request = require('supertest'); -import { _extractProjects } from './projects'; - -// const API_HOST = process.env.REACT_APP_API_HOST; -// const API_PORT = process.env.REACT_APP_API_PORT; - -// const API_URL = (API_PORT && `${API_HOST}:${API_PORT}`) || API_HOST || 'https://api-dev-biohubbc.apps.silver.devops.gov.bc.ca'; -// const KEYCLOAK_URL = -// process.env.KEYCLOAK_URL || 'https://dev.oidc.gov.bc.ca/auth/realms/35r1iman/protocol/openid-connect/certs'; - -describe('Unit Testing: GET /projects - Test database query result parsing', () => { - it('should return empty array if query result was empty', function () { - const rows: any[] = []; - const projects: any[] = _extractProjects(rows); - - expect(projects).to.be.an('array'); - expect(projects).to.have.length(0); - }); - - it('should return an array of one element if query result contains one row', function () { - const rows: any[] = []; - - rows.push({ - id: 1, - name: 'Project BioHub', - start_date: '2021/01/01', - end_date: '2022/12/31', - location_description: 'Here' - }); - - const projects: any[] = _extractProjects(rows); - - expect(projects).to.be.an('array'); - expect(projects).to.have.length(1); - expect(projects[0]).to.have.property('name', 'Project BioHub'); - }); -}); - -// TODO: Come back to do integration test. - -// describe('Integration Testing: GET /projects', () => { -// it('should require authorization', function (done) { -// request(API_URL) -// .get('/api/projects') -// .expect(401) -// .end(function (err: any) { -// if (err) return done(err); -// done(); -// }); -// }); -// it('should make a connection to the API', function (done) { -// request(API_URL) -// .get('/api/api-docs') -// .expect(200) -// .end(function (err: any) { -// if (err) return done(err); -// done(); -// }); -// }); -// var auth: any = {}; -// before(loginUser(auth)); -// it('should respond with JSON array', function (done: any) { -// request(API_URL) -// .get('/api/projects') -// .set('Authorization', 'bearer ' + auth.token) -// .expect(401) -// .expect('Content-Type', /json/) -// .end(function(err: any, res: any) { -// if (err) return done(err); -// res.body.should.be.instanceof(Array); -// done(); -// }); -// }); -// }); -// function loginUser(auth: any) { -// return function(done: any) { -// request(KEYCLOAK_URL) -// .post('/protocol/openid-connect/certs') -// .send({ -// email: 'test@test.com', -// password: 'test' -// }) -// .expect(200) -// .end(onResponse); - -// function onResponse(err: any, res: any) { -// auth.token = res.body.token; -// return done(); -// } -// }; -// }; diff --git a/api/src/paths/public/project/list.test.ts b/api/src/paths/public/project/list.test.ts new file mode 100644 index 0000000000..e5305bdc5f --- /dev/null +++ b/api/src/paths/public/project/list.test.ts @@ -0,0 +1,94 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import { ProjectService } from '../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { GET, getPublicProjectsList } from './list'; + +chai.use(sinonChai); + +describe('list', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('getPublicProjectsList', () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns an empty array if no project ids are found', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'getPublicProjectsList').resolves([]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getPublicProjectsList(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql([]); + }); + + it('returns an array of projects', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const mockProject1 = ({ project: { project_id: 1 } } as unknown) as any; + const mockProject2 = ({ project: { project_id: 2 } } as unknown) as any; + + sinon.stub(ProjectService.prototype, 'getPublicProjectsList').resolves([mockProject1, mockProject2]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getPublicProjectsList(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.jsonValue).to.eql([mockProject1, mockProject2]); + expect(mockRes.statusValue).to.equal(200); + }); + + it('catches error, calls rollback, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'getPublicProjectsList').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getPublicProjectsList(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.release).to.have.been.called; + + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/public/project/list.ts b/api/src/paths/public/project/list.ts new file mode 100644 index 0000000000..469b5d8815 --- /dev/null +++ b/api/src/paths/public/project/list.ts @@ -0,0 +1,70 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../database/db'; +import { projectIdResponseObject } from '../../../openapi/schemas/project'; +import { ProjectService } from '../../../services/project-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/public/projects'); + +export const GET: Operation = [getPublicProjectsList()]; + +GET.apiDoc = { + description: 'Gets a list of public facing (published) projects.', + tags: ['public', 'projects'], + responses: { + 200: { + description: 'Project response object.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + ...(projectIdResponseObject as object) + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get all public facing (published) projects. + * + * @returns {RequestHandler} + */ +export function getPublicProjectsList(): RequestHandler { + return async (req, res) => { + const connection = getAPIUserDBConnection(); + + try { + await connection.open(); + + const projectService = new ProjectService(connection); + + const projects = await projectService.getPublicProjectsList(); + + await connection.commit(); + + return res.status(200).json(projects); + } catch (error) { + defaultLog.error({ label: 'getPublicProjectsList', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/public/project/{projectId}/attachments/list.test.ts b/api/src/paths/public/project/{projectId}/attachments/list.test.ts index 59e7a997f1..c16d347fab 100644 --- a/api/src/paths/public/project/{projectId}/attachments/list.test.ts +++ b/api/src/paths/public/project/{projectId}/attachments/list.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as list from './list'; -import * as db from '../../../../../database/db'; -import * as project_queries from '../../../../../queries/public/project-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../../../../database/db'; +import { HTTPError } from '../../../../../errors/custom-error'; +import public_queries from '../../../../../queries/public'; import { getMockDBConnection } from '../../../../../__mocks__/db'; +import * as list from './list'; chai.use(sinonChai); @@ -49,8 +50,8 @@ describe('getPublicProjectAttachments', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -62,7 +63,7 @@ describe('getPublicProjectAttachments', () => { } }); - sinon.stub(project_queries, 'getPublicProjectAttachmentsSQL').returns(null); + sinon.stub(public_queries, 'getPublicProjectAttachmentsSQL').returns(null); try { const result = list.getPublicProjectAttachments(); @@ -70,8 +71,8 @@ describe('getPublicProjectAttachments', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -89,7 +90,7 @@ describe('getPublicProjectAttachments', () => { update_date: '', file_size: 50, file_type: 'Image', - is_secured: false + is_secured: null } ] }) @@ -103,7 +104,7 @@ describe('getPublicProjectAttachments', () => { update_date: '', file_size: 50, file_type: 'Report', - is_secured: false + is_secured: null } ] }); @@ -116,7 +117,7 @@ describe('getPublicProjectAttachments', () => { query: mockQuery }); - sinon.stub(project_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); const result = list.getPublicProjectAttachments(); @@ -124,8 +125,8 @@ describe('getPublicProjectAttachments', () => { expect(actualResult).to.be.eql({ attachmentsList: [ - { fileName: 'name1', fileType: 'Image', id: 13, lastModified: '2020-01-01', size: 50, securityToken: false }, - { fileName: 'name2', fileType: 'Report', id: 14, lastModified: '2020-01-01', size: 50, securityToken: false } + { fileName: 'name1', fileType: 'Image', id: 13, lastModified: '2020-01-01', size: 50, securityToken: 'false' }, + { fileName: 'name2', fileType: 'Report', id: 14, lastModified: '2020-01-01', size: 50, securityToken: 'false' } ] }); }); @@ -144,7 +145,7 @@ describe('getPublicProjectAttachments', () => { update_date: '2020-04-04', file_size: 50, file_type: 'Image', - is_secured: false + is_secured: null } ] }) @@ -158,7 +159,7 @@ describe('getPublicProjectAttachments', () => { update_date: '2020-04-04', file_size: 50, file_type: 'Report', - is_secured: false + is_secured: null } ] }); @@ -171,7 +172,7 @@ describe('getPublicProjectAttachments', () => { query: mockQuery }); - sinon.stub(project_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); const result = list.getPublicProjectAttachments(); @@ -179,8 +180,8 @@ describe('getPublicProjectAttachments', () => { expect(actualResult).to.be.eql({ attachmentsList: [ - { fileName: 'name1', fileType: 'Image', id: 13, lastModified: '2020-04-04', size: 50, securityToken: false }, - { fileName: 'name2', fileType: 'Report', id: 14, lastModified: '2020-04-04', size: 50, securityToken: false } + { fileName: 'name1', fileType: 'Image', id: 13, lastModified: '2020-04-04', size: 50, securityToken: 'false' }, + { fileName: 'name2', fileType: 'Report', id: 14, lastModified: '2020-04-04', size: 50, securityToken: 'false' } ] }); }); @@ -198,7 +199,7 @@ describe('getPublicProjectAttachments', () => { query: mockQuery }); - sinon.stub(project_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicProjectAttachmentsSQL').returns(SQL`something`); const result = list.getPublicProjectAttachments(); diff --git a/api/src/paths/public/project/{projectId}/attachments/list.ts b/api/src/paths/public/project/{projectId}/attachments/list.ts index 7f5dc4aeb2..34e101599d 100644 --- a/api/src/paths/public/project/{projectId}/attachments/list.ts +++ b/api/src/paths/public/project/{projectId}/attachments/list.ts @@ -1,15 +1,10 @@ -'use strict'; - -import { getAPIUserDBConnection } from '../../../../../database/db'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../errors/CustomError'; -import { getLogger } from '../../../../../utils/logger'; -import { - getPublicProjectAttachmentsSQL, - getPublicProjectReportAttachmentsSQL -} from '../../../../../queries/public/project-queries'; +import { getAPIUserDBConnection } from '../../../../../database/db'; +import { HTTP400 } from '../../../../../errors/custom-error'; import { GetPublicAttachmentsData } from '../../../../../models/public/project'; +import { queries } from '../../../../../queries/queries'; +import { getLogger } from '../../../../../utils/logger'; const defaultLog = getLogger('/api/public/project/{projectId}/attachments/list'); @@ -30,21 +25,38 @@ GET.apiDoc = { ], responses: { 200: { - description: 'Public (published) project get response file description array.', + description: 'Project get response file description array.', content: { 'application/json': { schema: { - type: 'array', - items: { - type: 'object', - properties: { - fileName: { - description: 'The file name of the attachment', - type: 'string' - }, - lastModified: { - description: 'The date the object was last modified', - type: 'string' + type: 'object', + properties: { + attachmentsList: { + type: 'array', + items: { + type: 'object', + required: ['id', 'fileName', 'fileType', 'lastModified', 'securityToken', 'size'], + properties: { + id: { + type: 'number' + }, + fileName: { + type: 'string' + }, + fileType: { + type: 'string' + }, + lastModified: { + type: 'string' + }, + securedToken: { + type: 'string', + enum: ['true', 'false'] + }, + size: { + type: 'number' + } + } } } } @@ -72,8 +84,10 @@ export function getPublicProjectAttachments(): RequestHandler { const connection = getAPIUserDBConnection(); try { - const getPublicProjectAttachmentsSQLStatement = getPublicProjectAttachmentsSQL(Number(req.params.projectId)); - const getPublicProjectReportAttachmentsSQLStatement = getPublicProjectReportAttachmentsSQL( + const getPublicProjectAttachmentsSQLStatement = queries.public.getPublicProjectAttachmentsSQL( + Number(req.params.projectId) + ); + const getPublicProjectReportAttachmentsSQLStatement = queries.public.getPublicProjectReportAttachmentsSQL( Number(req.params.projectId) ); diff --git a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts index 069d7998ef..12731ccb42 100644 --- a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts +++ b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.test.ts @@ -2,16 +2,18 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as get_signed_url from './getSignedUrl'; -import * as db from '../../../../../../database/db'; -import * as project_queries from '../../../../../../queries/public/project-queries'; import SQL from 'sql-template-strings'; +import { ATTACHMENT_TYPE } from '../../../../../../constants/attachments'; +import * as db from '../../../../../../database/db'; +import { HTTPError } from '../../../../../../errors/custom-error'; +import public_queries from '../../../../../../queries/public'; import * as file_utils from '../../../../../../utils/file-utils'; import { getMockDBConnection } from '../../../../../../__mocks__/db'; +import * as get_signed_url from './getSignedUrl'; chai.use(sinonChai); -describe('getSingleAttachmentURL', () => { +describe('getAttachmentSignedURL', () => { afterEach(() => { sinon.restore(); }); @@ -24,8 +26,8 @@ describe('getSingleAttachmentURL', () => { projectId: 1, attachmentId: 2 }, - body: { - attachmentType: 'Image' + query: { + attachmentType: 'Other' } } as any; @@ -45,7 +47,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, @@ -54,8 +56,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `projectId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); } }); @@ -63,7 +65,7 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getAttachmentSignedURL(); await result( { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, @@ -72,8 +74,8 @@ describe('getSingleAttachmentURL', () => { ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param `attachmentId`'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); } }); @@ -81,38 +83,17 @@ describe('getSingleAttachmentURL', () => { sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getAttachmentSignedURL(); await result( - { ...sampleReq, body: { ...sampleReq.body, attachmentType: null } }, + { ...sampleReq, query: { ...sampleReq.query, attachmentType: null } }, (null as unknown) as any, (null as unknown) as any ); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param `attachmentType`'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(project_queries, 'getPublicProjectAttachmentS3KeySQL').returns(null); - - try { - const result = get_signed_url.getSingleAttachmentURL(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required query param `attachmentType`'); } }); @@ -129,36 +110,123 @@ describe('getSingleAttachmentURL', () => { query: mockQuery }); - sinon.stub(project_queries, 'getPublicProjectAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(public_queries, 'getPublicProjectAttachmentS3KeySQL').returns(SQL`some query`); sinon.stub(file_utils, 'getS3SignedURL').resolves(null); - const result = get_signed_url.getSingleAttachmentURL(); + const result = get_signed_url.getAttachmentSignedURL(); await result(sampleReq, sampleRes as any, (null as unknown) as any); expect(actualResult).to.equal(null); }); - it('should return the signed url response on success', async () => { - const mockQuery = sinon.stub(); + describe('non report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); - mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + sinon.stub(public_queries, 'getPublicProjectAttachmentS3KeySQL').returns(null); - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery + try { + const result = get_signed_url.getAttachmentSignedURL(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build attachment S3 key SQLstatement'); + } }); - sinon.stub(project_queries, 'getPublicProjectAttachmentS3KeySQL').returns(SQL`some query`); - sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + it('should return the attachment signed url response on success', async () => { + const mockQuery = sinon.stub(); - const result = get_signed_url.getSingleAttachmentURL(); + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(public_queries, 'getPublicProjectAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getAttachmentSignedURL(); - expect(actualResult).to.eql('myurlsigned.com'); + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql('myurlsigned.com'); + }); + }); + + describe('report attachments', () => { + it('should throw a 400 error when no sql statement returned', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(public_queries, 'getPublicProjectReportAttachmentS3KeySQL').returns(null); + + try { + const result = get_signed_url.getAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build report attachment S3 key SQLstatement'); + } + }); + + it('should return the report attachment signed url response on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ rows: [{ key: 's3Key' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(public_queries, 'getPublicProjectReportAttachmentS3KeySQL').returns(SQL`some query`); + sinon.stub(file_utils, 'getS3SignedURL').resolves('myurlsigned.com'); + + const result = get_signed_url.getAttachmentSignedURL(); + + await result( + { + ...sampleReq, + query: { + attachmentType: ATTACHMENT_TYPE.REPORT + } + }, + sampleRes as any, + (null as unknown) as any + ); + + expect(actualResult).to.eql('myurlsigned.com'); + }); }); }); diff --git a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts index 8bc3ade374..50bec442e7 100644 --- a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts +++ b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl.ts @@ -1,22 +1,18 @@ -'use strict'; - import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { HTTP400 } from '../../../../../../errors/CustomError'; -import { getLogger } from '../../../../../../utils/logger'; -import { getAPIUserDBConnection } from '../../../../../../database/db'; +import { ATTACHMENT_TYPE } from '../../../../../../constants/attachments'; +import { getAPIUserDBConnection, IDBConnection } from '../../../../../../database/db'; +import { HTTP400 } from '../../../../../../errors/custom-error'; +import { queries } from '../../../../../../queries/queries'; import { getS3SignedURL } from '../../../../../../utils/file-utils'; -import { - getPublicProjectAttachmentS3KeySQL, - getPublicProjectReportAttachmentS3KeySQL -} from '../../../../../../queries/public/project-queries'; +import { getLogger } from '../../../../../../utils/logger'; const defaultLog = getLogger('/api/public/project/{projectId}/attachments/{attachmentId}/getSignedUrl'); -export const POST: Operation = [getSingleAttachmentURL()]; +export const GET: Operation = [getAttachmentSignedURL()]; -POST.apiDoc = { - description: 'Retrieves the signed url of an attachment in a public (published) project by its file name.', +GET.apiDoc = { + description: 'Retrieves the signed url of a public project attachment.', tags: ['attachment'], parameters: [ { @@ -34,41 +30,54 @@ POST.apiDoc = { type: 'number' }, required: true + }, + { + in: 'query', + name: 'attachmentType', + schema: { + type: 'string', + enum: ['Report', 'Other'] + }, + required: true } ], - requestBody: { - description: 'Current attachment type for public (published) project attachment.', - content: { - 'application/json': { - schema: { - type: 'object' - } - } - } - }, responses: { 200: { description: 'Response containing the signed url of an attachment.', content: { 'text/plain': { schema: { - type: 'number' + type: 'string' } } } }, + 400: { + $ref: '#/components/responses/400' + }, 401: { $ref: '#/components/responses/401' }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, default: { $ref: '#/components/responses/default' } } }; -export function getSingleAttachmentURL(): RequestHandler { +export function getAttachmentSignedURL(): RequestHandler { return async (req, res) => { - defaultLog.debug({ label: 'Get single attachment url', message: 'params', req_params: req.params }); + defaultLog.debug({ + label: 'getAttachmentSignedURL', + message: 'params', + req_params: req.params, + req_query: req.query + }); if (!req.params.projectId) { throw new HTTP400('Missing required path param `projectId`'); @@ -78,33 +87,35 @@ export function getSingleAttachmentURL(): RequestHandler { throw new HTTP400('Missing required path param `attachmentId`'); } - if (!req.body || !req.body.attachmentType) { - throw new HTTP400('Missing required body param `attachmentType`'); + if (!req.query.attachmentType) { + throw new HTTP400('Missing required query param `attachmentType`'); } const connection = getAPIUserDBConnection(); - try { - const getProjectAttachmentS3KeySQLStatement = - req.body.attachmentType === 'Report' - ? getPublicProjectReportAttachmentS3KeySQL(Number(req.params.attachmentId)) - : getPublicProjectAttachmentS3KeySQL(Number(req.params.attachmentId)); - - if (!getProjectAttachmentS3KeySQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } + await connection.open(); + try { await connection.open(); - const result = await connection.query( - getProjectAttachmentS3KeySQLStatement.text, - getProjectAttachmentS3KeySQLStatement.values - ); + let s3Key; + + if (req.query.attachmentType === ATTACHMENT_TYPE.REPORT) { + s3Key = await getPublicProjectReportAttachmentS3Key( + Number(req.params.projectId), + Number(req.params.attachmentId), + connection + ); + } else { + s3Key = await getPublicProjectAttachmentS3Key( + Number(req.params.projectId), + Number(req.params.attachmentId), + connection + ); + } await connection.commit(); - const s3Key = result && result.rows.length && result.rows[0].key; - const s3SignedUrl = s3Key && (await getS3SignedURL(s3Key)); if (!s3SignedUrl) { @@ -113,7 +124,7 @@ export function getSingleAttachmentURL(): RequestHandler { return res.status(200).json(s3SignedUrl); } catch (error) { - defaultLog.error({ label: 'getSingleAttachmentURL', message: 'error', error }); + defaultLog.error({ label: 'getAttachmentSignedURL', message: 'error', error }); await connection.rollback(); throw error; } finally { @@ -121,3 +132,43 @@ export function getSingleAttachmentURL(): RequestHandler { } }; } + +export const getPublicProjectAttachmentS3Key = async ( + projectId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.public.getPublicProjectAttachmentS3KeySQL(projectId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; + +export const getPublicProjectReportAttachmentS3Key = async ( + projectId: number, + attachmentId: number, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.public.getPublicProjectReportAttachmentS3KeySQL(projectId, attachmentId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build report attachment S3 key SQLstatement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response?.rows?.[0]) { + throw new HTTP400('Failed to get attachment S3 key'); + } + + return response.rows[0].key; +}; diff --git a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts new file mode 100644 index 0000000000..46d5acd147 --- /dev/null +++ b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.test.ts @@ -0,0 +1,161 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../../../../database/db'; +import { HTTPError } from '../../../../../../../errors/custom-error'; +import public_queries from '../../../../../../../queries/public'; +import { getMockDBConnection } from '../../../../../../../__mocks__/db'; +import * as get_project_metadata from './get'; + +chai.use(sinonChai); + +describe('gets metadata for a project report', () => { + const dbConnectionObj = getMockDBConnection(); + + const sampleReq = { + keycloak_token: {}, + body: {}, + params: { + projectId: 1, + attachmentId: 1 + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no projectId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_project_metadata.getPublicReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, projectId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `projectId`'); + } + }); + + it('should throw a 400 error when no attachmentId is provided', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = get_project_metadata.getPublicReportMetaData(); + await result( + { ...sampleReq, params: { ...sampleReq.params, attachmentId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param `attachmentId`'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectReportAttachmentSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(public_queries, 'getPublicProjectReportAttachmentSQL').returns(null); + + try { + const result = get_project_metadata.getPublicReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectReportAuthorsSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(public_queries, 'getProjectReportAuthorsSQL').returns(null); + + try { + const result = get_project_metadata.getPublicReportMetaData(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build metadata SQLStatement'); + } + }); + + it('should return a project report metadata, on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ + rowCount: 1, + rows: [ + { + attachment_id: 1, + title: 'My report', + update_date: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1' + } + ] + }); + mockQuery.onCall(1).resolves({ rowCount: 1, rows: [{ first_name: 'John', last_name: 'Smith' }] }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(public_queries, 'getPublicProjectReportAttachmentSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getProjectReportAuthorsSQL').returns(SQL`something`); + + const result = get_project_metadata.getPublicReportMetaData(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.be.eql({ + attachment_id: 1, + title: 'My report', + last_modified: '2020-10-10', + description: 'some description', + year_published: 2020, + revision_count: '1', + authors: [{ first_name: 'John', last_name: 'Smith' }] + }); + }); +}); diff --git a/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.ts b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.ts new file mode 100644 index 0000000000..7c5a0c0858 --- /dev/null +++ b/api/src/paths/public/project/{projectId}/attachments/{attachmentId}/metadata/get.ts @@ -0,0 +1,177 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../../../../../database/db'; +import { HTTP400 } from '../../../../../../../errors/custom-error'; +import { GetReportAttachmentMetadata } from '../../../../../../../models/project-survey-attachments'; +import { queries } from '../../../../../../../queries/queries'; +import { getLogger } from '../../../../../../../utils/logger'; + +const defaultLog = getLogger('/api/project/{projectId}/attachments/{attachmentId}/getSignedUrl'); + +export const GET: Operation = [getPublicReportMetaData()]; + +GET.apiDoc = { + description: 'Retrieves the report metadata of a project attachment if filetype is Report.', + tags: ['attachment'], + parameters: [ + { + in: 'path', + name: 'projectId', + schema: { + type: 'number' + }, + required: true + }, + { + in: 'path', + name: 'attachmentId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Response of the report metadata', + content: { + 'application/json': { + schema: { + title: 'metadata get response object', + type: 'object', + required: [ + 'attachment_id', + 'title', + 'last_modified', + 'description', + 'year_published', + 'revision_count', + 'authors' + ], + properties: { + attachment_id: { + description: 'Report metadata attachment id', + type: 'number' + }, + title: { + description: 'Report metadata attachment title ', + type: 'string' + }, + last_modified: { + description: 'Report metadata last modified', + type: 'string' + }, + description: { + description: 'Report metadata description', + type: 'string' + }, + year_published: { + description: 'Report metadata year published', + type: 'number' + }, + revision_count: { + description: 'Report metadata revision count', + type: 'number' + }, + authors: { + description: 'Report metadata author object', + type: 'array', + items: { + type: 'object', + required: ['first_name', 'last_name'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function getPublicReportMetaData(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'getSurveyReportMetaData', + message: 'params', + req_params: req.params, + req_query: req.query + }); + + if (!req.params.projectId) { + throw new HTTP400('Missing required path param `projectId`'); + } + + if (!req.params.attachmentId) { + throw new HTTP400('Missing required path param `attachmentId`'); + } + + const connection = getAPIUserDBConnection(); + + try { + const getPublicProjectReportAttachmentSQLStatement = queries.public.getPublicProjectReportAttachmentSQL( + Number(req.params.projectId), + Number(req.params.attachmentId) + ); + + const getProjectReportAuthorsSQLStatement = queries.public.getProjectReportAuthorsSQL( + Number(req.params.attachmentId) + ); + + if (!getPublicProjectReportAttachmentSQLStatement || !getProjectReportAuthorsSQLStatement) { + throw new HTTP400('Failed to build metadata SQLStatement'); + } + + await connection.open(); + + const reportMetaData = await connection.query( + getPublicProjectReportAttachmentSQLStatement.text, + getPublicProjectReportAttachmentSQLStatement.values + ); + + const reportAuthorsData = await connection.query( + getProjectReportAuthorsSQLStatement.text, + getProjectReportAuthorsSQLStatement.values + ); + + await connection.commit(); + + const getReportMetaData = reportMetaData && reportMetaData.rows[0]; + + const getReportAuthorsData = reportAuthorsData && reportAuthorsData.rows; + + const reportMetaObj = new GetReportAttachmentMetadata(getReportMetaData, getReportAuthorsData); + + return res.status(200).json(reportMetaObj); + } catch (error) { + defaultLog.error({ label: 'getPublicReportMetadata', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/public/project/{projectId}/view.test.ts b/api/src/paths/public/project/{projectId}/view.test.ts new file mode 100644 index 0000000000..dab712bc12 --- /dev/null +++ b/api/src/paths/public/project/{projectId}/view.test.ts @@ -0,0 +1,72 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { ProjectService } from '../../../../services/project-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import { GET, getPublicProjectForView } from './view'; + +chai.use(sinonChai); + +describe('project/{projectId}/view', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('viewPublicProject', () => { + afterEach(() => { + sinon.restore(); + }); + + it('fetches a project', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const viewProjectResult = { id: 1 }; + + sinon.stub(ProjectService.prototype, 'getPublicProjectById').resolves(viewProjectResult as any); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getPublicProjectForView(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql(viewProjectResult); + }); + + it('catches and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(ProjectService.prototype, 'getPublicProjectById').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getPublicProjectForView(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(dbConnectionObj.release).to.have.been.called; + + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/public/project/{projectId}/view.ts b/api/src/paths/public/project/{projectId}/view.ts index f0aa941807..7485a53408 100644 --- a/api/src/paths/public/project/{projectId}/view.ts +++ b/api/src/paths/public/project/{projectId}/view.ts @@ -1,33 +1,13 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { - getActivitiesByPublicProjectSQL, - getStakeholderPartnershipsByPublicProjectSQL, - getFundingSourceByPublicProjectSQL, - getLocationByPublicProjectSQL, - getPublicProjectPermitsSQL, - getPublicProjectSQL, - getIndigenousPartnershipsByPublicProjectSQL, - getIUCNActionClassificationByPublicProjectSQL -} from '../../../../queries/public/project-queries'; import { getAPIUserDBConnection } from '../../../../database/db'; -import { HTTP400 } from '../../../../errors/CustomError'; -import { - GetIUCNClassificationData, - GetObjectivesData, - GetPartnershipsData, - GetLocationData, - GetPermitData -} from '../../../../models/project-view'; -import { GetPublicProjectData, GetPublicCoordinatorData } from '../../../../models/public/project'; -import { GetFundingData } from '../../../../models/project-view-update'; -import { projectViewGetResponseObject } from '../../../../openapi/schemas/project'; +import { geoJsonFeature } from '../../../../openapi/schemas/geoJson'; +import { ProjectService } from '../../../../services/project-service'; import { getLogger } from '../../../../utils/logger'; -import { logRequest } from '../../../../utils/path-utils'; const defaultLog = getLogger('paths/public/project/{projectId}/view'); -export const GET: Operation = [logRequest('paths/public/project/{projectId}/view', 'GET'), getPublicProjectForView()]; +export const GET: Operation = [getPublicProjectForView()]; GET.apiDoc = { description: 'Get a public (published) project, for view-only purposes.', @@ -48,7 +28,242 @@ GET.apiDoc = { content: { 'application/json': { schema: { - ...(projectViewGetResponseObject as object) + title: 'Project get response object, for view purposes', + type: 'object', + required: [ + 'id', + 'project', + 'permit', + 'coordinator', + 'objectives', + 'location', + 'iucn', + 'funding', + 'partnerships' + ], + properties: { + id: { + description: 'Project id', + type: 'number' + }, + project: { + description: 'Basic project metadata', + type: 'object', + required: [ + 'project_name', + 'project_type', + 'project_activities', + 'start_date', + 'end_date', + 'comments', + 'completion_status', + 'publish_date' + ], + properties: { + project_name: { + type: 'string' + }, + project_type: { + type: 'number' + }, + project_activities: { + type: 'array', + items: { + type: 'number' + } + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the project end date' + }, + comments: { + type: 'string', + description: 'Comments' + }, + completion_status: { + description: 'Status of the project being active/completed', + type: 'string' + }, + publish_date: { + description: 'Status of the project being published/unpublished', + format: 'date', + type: 'string' + } + } + }, + permit: { + type: 'object', + required: ['permits'], + properties: { + permits: { + type: 'array', + items: { + title: 'Project permit', + type: 'object', + properties: { + permit_number: { + type: 'string' + }, + permit_type: { + type: 'string' + } + } + } + } + } + }, + coordinator: { + title: 'Project coordinator', + type: 'object', + required: ['first_name', 'last_name', 'email_address', 'coordinator_agency', 'share_contact_details'], + properties: { + first_name: { + type: 'string' + }, + last_name: { + type: 'string' + }, + email_address: { + type: 'string' + }, + coordinator_agency: { + type: 'string' + }, + share_contact_details: { + type: 'string', + enum: ['true', 'false'] + } + } + }, + objectives: { + description: 'The project objectives and caveats', + type: 'object', + required: ['objectives', 'caveats'], + properties: { + objectives: { + type: 'string' + }, + caveats: { + type: 'string' + } + } + }, + location: { + description: 'The project location object', + type: 'object', + required: ['location_description', 'geometry'], + properties: { + location_description: { + type: 'string' + }, + geometry: { + type: 'array', + items: { + ...(geoJsonFeature as object) + } + } + } + }, + iucn: { + description: 'The International Union for Conservation of Nature number', + type: 'object', + required: ['classificationDetails'], + properties: { + classificationDetails: { + type: 'array', + items: { + type: 'object', + properties: { + classification: { + type: 'number' + }, + subClassification1: { + type: 'number' + }, + subClassification2: { + type: 'number' + } + } + } + } + } + }, + funding: { + description: 'The project funding details', + type: 'object', + required: ['fundingSources'], + properties: { + fundingSources: { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number' + }, + agency_id: { + type: 'number' + }, + investment_action_category: { + type: 'number' + }, + investment_action_category_name: { + type: 'string' + }, + agency_name: { + type: 'string' + }, + funding_amount: { + type: 'number' + }, + start_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding start date' + }, + end_date: { + type: 'string', + format: 'date', + description: 'ISO 8601 date string for the funding end date' + }, + agency_project_id: { + type: 'string', + nullable: true + }, + revision_count: { + type: 'number' + } + } + } + } + } + }, + partnerships: { + description: 'The project partners', + type: 'object', + required: ['indigenous_partnerships', 'stakeholder_partnerships'], + properties: { + indigenous_partnerships: { + type: 'array', + items: { + type: 'number' + } + }, + stakeholder_partnerships: { + type: 'array', + items: { + type: 'string' + } + } + } + } + } } } } @@ -76,116 +291,18 @@ GET.apiDoc = { * * @returns {RequestHandler} */ -function getPublicProjectForView(): RequestHandler { +export function getPublicProjectForView(): RequestHandler { return async (req, res) => { const connection = getAPIUserDBConnection(); try { - const getProjectSQLStatement = getPublicProjectSQL(Number(req.params.projectId)); - const getProjectPermitsSQLStatement = getPublicProjectPermitsSQL(Number(req.params.projectId)); - const getProjectLocationSQLStatement = getLocationByPublicProjectSQL(Number(req.params.projectId)); - const getProjectActivitiesSQLStatement = getActivitiesByPublicProjectSQL(Number(req.params.projectId)); - const getProjectIUCNActionClassificationSQLStatement = getIUCNActionClassificationByPublicProjectSQL( - Number(req.params.projectId) - ); - const getProjectFundingSourceSQLStatement = getFundingSourceByPublicProjectSQL(Number(req.params.projectId)); - const getProjectIndigenousPartnershipsSQLStatement = getIndigenousPartnershipsByPublicProjectSQL( - Number(req.params.projectId) - ); - const getProjectStakeholderPartnershipsSQLStatement = getStakeholderPartnershipsByPublicProjectSQL( - Number(req.params.projectId) - ); - - if ( - !getProjectSQLStatement || - !getProjectPermitsSQLStatement || - !getProjectLocationSQLStatement || - !getProjectActivitiesSQLStatement || - !getProjectIUCNActionClassificationSQLStatement || - !getProjectFundingSourceSQLStatement || - !getProjectIndigenousPartnershipsSQLStatement || - !getProjectStakeholderPartnershipsSQLStatement - ) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const [ - projectData, - permitData, - locationData, - activityData, - iucnClassificationData, - fundingData, - indigenousPartnerships, - stakeholderPartnerships - ] = await Promise.all([ - await connection.query(getProjectSQLStatement.text, getProjectSQLStatement.values), - await connection.query(getProjectPermitsSQLStatement.text, getProjectPermitsSQLStatement.values), - await connection.query(getProjectLocationSQLStatement.text, getProjectLocationSQLStatement.values), - await connection.query(getProjectActivitiesSQLStatement.text, getProjectActivitiesSQLStatement.values), - await connection.query( - getProjectIUCNActionClassificationSQLStatement.text, - getProjectIUCNActionClassificationSQLStatement.values - ), - await connection.query(getProjectFundingSourceSQLStatement.text, getProjectFundingSourceSQLStatement.values), - await connection.query( - getProjectIndigenousPartnershipsSQLStatement.text, - getProjectIndigenousPartnershipsSQLStatement.values - ), - await connection.query( - getProjectStakeholderPartnershipsSQLStatement.text, - getProjectStakeholderPartnershipsSQLStatement.values - ) - ]); - - await connection.commit(); - - const getProjectData = - (projectData && - projectData.rows && - activityData && - activityData.rows && - new GetPublicProjectData(projectData.rows[0], activityData.rows)) || - null; - - const getPermitData = (permitData && permitData.rows && new GetPermitData(permitData.rows)) || null; + const projectService = new ProjectService(connection); - const getObjectivesData = (projectData && projectData.rows && new GetObjectivesData(projectData.rows[0])) || null; + const result = await projectService.getPublicProjectById(Number(req.params.projectId)); - const getLocationData = (locationData && locationData.rows && new GetLocationData(locationData.rows)) || null; - - const getCoordinatorData = - (projectData && projectData.rows && new GetPublicCoordinatorData(projectData.rows[0])) || null; - - const getPartnershipsData = - (indigenousPartnerships && - indigenousPartnerships.rows && - stakeholderPartnerships && - stakeholderPartnerships.rows && - new GetPartnershipsData(indigenousPartnerships.rows, stakeholderPartnerships.rows)) || - null; - - const getIUCNClassificationData = - (iucnClassificationData && - iucnClassificationData.rows && - new GetIUCNClassificationData(iucnClassificationData.rows)) || - null; - - const getFundingData = (fundingData && fundingData.rows && new GetFundingData(fundingData.rows)) || null; - - const result = { - id: req.params.projectId, - project: getProjectData, - permit: getPermitData, - coordinator: getCoordinatorData, - objectives: getObjectivesData, - location: getLocationData, - iucn: getIUCNClassificationData, - funding: getFundingData, - partnerships: getPartnershipsData - }; + await connection.commit(); return res.status(200).json(result); } catch (error) { diff --git a/api/src/paths/public/projects.test.ts b/api/src/paths/public/projects.test.ts deleted file mode 100644 index c9c31001f3..0000000000 --- a/api/src/paths/public/projects.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as projects from './projects'; -import * as db from '../../database/db'; -import * as project_queries from '../../queries/public/project-queries'; -import SQL from 'sql-template-strings'; -import { COMPLETION_STATUS } from '../../constants/status'; -import { getMockDBConnection } from '../../__mocks__/db'; - -chai.use(sinonChai); - -describe('getPublicProjectsList', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {} - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - it('should throw a 400 error when no sql statement returned for getPublicProjectListSQL', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(project_queries, 'getPublicProjectListSQL').returns(null); - - try { - const result = projects.getPublicProjectsList(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should return all public projects on success', async () => { - const projectsList = [ - { - id: 1, - name: 'name', - start_date: '2020/04/04', - end_date: '2020/05/05', - coordinator_agency_name: 'agency', - project_type: 'type', - permits_list: [123, 1233] - } - ]; - - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: projectsList }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(project_queries, 'getPublicProjectListSQL').returns(SQL`some query`); - - const result = projects.getPublicProjectsList(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql([ - { - id: projectsList[0].id, - name: projectsList[0].name, - start_date: projectsList[0].start_date, - end_date: projectsList[0].end_date, - coordinator_agency: projectsList[0].coordinator_agency_name, - completion_status: COMPLETION_STATUS.COMPLETED, - project_type: projectsList[0].project_type, - permits_list: projectsList[0].permits_list - } - ]); - }); -}); diff --git a/api/src/paths/public/projects.ts b/api/src/paths/public/projects.ts deleted file mode 100644 index 5ae7d24174..0000000000 --- a/api/src/paths/public/projects.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { COMPLETION_STATUS } from '../../constants/status'; -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import moment from 'moment'; -import { getAPIUserDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { projectIdResponseObject } from '../../openapi/schemas/project'; -import { getPublicProjectListSQL } from '../../queries/public/project-queries'; -import { getLogger } from '../../utils/logger'; -import { logRequest } from '../../utils/path-utils'; - -const defaultLog = getLogger('paths/public/projects'); - -export const GET: Operation = [logRequest('paths/public/projects', 'POST'), getPublicProjectsList()]; - -GET.apiDoc = { - description: 'Gets a list of public facing (published) projects.', - tags: ['public', 'projects'], - responses: { - 200: { - description: 'Project response object.', - content: { - 'application/json': { - schema: { - type: 'array', - items: { - ...(projectIdResponseObject as object) - } - } - } - } - }, - 400: { - $ref: '#/components/responses/400' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -/** - * Get all public facing (published) projects. - * - * @returns {RequestHandler} - */ -export function getPublicProjectsList(): RequestHandler { - return async (req, res) => { - const connection = getAPIUserDBConnection(); - - try { - const getProjectListSQLStatement = getPublicProjectListSQL(); - - if (!getProjectListSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - await connection.open(); - - const getProjectListResponse = await connection.query( - getProjectListSQLStatement.text, - getProjectListSQLStatement.values - ); - - await connection.commit(); - - let rows: any[] = []; - - if (getProjectListResponse && getProjectListResponse.rows) { - rows = getProjectListResponse.rows; - } - - const result: any[] = _extractProjects(rows); - - return res.status(200).json(result); - } catch (error) { - defaultLog.error({ label: 'getPublicProjectsList', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} - -/** - * Extract an array of project data from DB query. - * - * @export - * @param {any[]} rows DB query result rows - * @return {any[]} An array of project data - */ -export function _extractProjects(rows: any[]): any[] { - if (!rows || !rows.length) { - return []; - } - - const projects: any[] = []; - - rows.forEach((row) => { - const project: any = { - id: row.id, - name: row.name, - start_date: row.start_date, - end_date: row.end_date, - coordinator_agency: row.coordinator_agency_name, - completion_status: - (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || - COMPLETION_STATUS.ACTIVE, - project_type: row.project_type, - permits_list: row.permits_list - }; - - projects.push(project); - }); - - return projects; -} diff --git a/api/src/paths/public/search.test.ts b/api/src/paths/public/search.test.ts index fb635423f4..1486e6a0bc 100644 --- a/api/src/paths/public/search.test.ts +++ b/api/src/paths/public/search.test.ts @@ -2,11 +2,12 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as search from './search'; -import * as db from '../../database/db'; -import * as search_queries from '../../queries/public/search-queries'; import SQL from 'sql-template-strings'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import public_queries from '../../queries/public'; import { getMockDBConnection } from '../../__mocks__/db'; +import * as search from './search'; chai.use(sinonChai); @@ -41,7 +42,7 @@ describe('search', () => { return 20; } }); - sinon.stub(search_queries, 'getPublicSpatialSearchResultsSQL').returns(null); + sinon.stub(public_queries, 'getPublicSpatialSearchResultsSQL').returns(null); try { const result = search.getSearchResults(); @@ -49,8 +50,8 @@ describe('search', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -66,7 +67,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(search_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); @@ -87,7 +88,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(search_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); @@ -116,7 +117,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(search_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); + sinon.stub(public_queries, 'getPublicSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); diff --git a/api/src/paths/public/search.ts b/api/src/paths/public/search.ts index 90f556a88c..830e439b64 100644 --- a/api/src/paths/public/search.ts +++ b/api/src/paths/public/search.ts @@ -1,16 +1,15 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { getAPIUserDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; +import { HTTP400 } from '../../errors/custom-error'; import { searchResponseObject } from '../../openapi/schemas/search'; +import { queries } from '../../queries/queries'; import { getLogger } from '../../utils/logger'; -import { logRequest } from '../../utils/path-utils'; -import { getPublicSpatialSearchResultsSQL } from '../../queries/public/search-queries'; import { _extractResults } from '../search'; const defaultLog = getLogger('paths/public/search'); -export const GET: Operation = [logRequest('paths/search', 'GET'), getSearchResults()]; +export const GET: Operation = [getSearchResults()]; GET.apiDoc = { description: 'Gets a list of published project geometries for public view', @@ -48,7 +47,7 @@ export function getSearchResults(): RequestHandler { const connection = getAPIUserDBConnection(); try { - const getSpatialSearchResultsSQLStatement = getPublicSpatialSearchResultsSQL(); + const getSpatialSearchResultsSQLStatement = queries.public.getPublicSpatialSearchResultsSQL(); if (!getSpatialSearchResultsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/search.test.ts b/api/src/paths/search.test.ts index 9a28a19190..4a2d0f1009 100644 --- a/api/src/paths/search.test.ts +++ b/api/src/paths/search.test.ts @@ -2,13 +2,14 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as search from './search'; -import * as db from '../database/db'; -import * as search_queries from '../queries/search-queries'; import SQL from 'sql-template-strings'; -import * as auth_utils from '../security/auth-utils'; import { SYSTEM_ROLE } from '../constants/roles'; +import * as db from '../database/db'; +import { HTTPError } from '../errors/custom-error'; +import search_queries from '../queries/search'; +import * as authorization from '../request-handlers/security/authorization'; import { getMockDBConnection } from '../__mocks__/db'; +import * as search from './search'; chai.use(sinonChai); @@ -46,7 +47,7 @@ describe('search', () => { return 20; } }); - sinon.stub(auth_utils, 'userHasValidSystemRoles').returns(true); + sinon.stub(authorization, 'userHasValidRole').returns(true); sinon.stub(search_queries, 'getSpatialSearchResultsSQL').returns(null); try { @@ -55,8 +56,8 @@ describe('search', () => { await result(sampleReq, (null as unknown) as any, (null as unknown) as any); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); } }); @@ -72,7 +73,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(auth_utils, 'userHasValidSystemRoles').returns(true); + sinon.stub(authorization, 'userHasValidRole').returns(true); sinon.stub(search_queries, 'getSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); @@ -94,7 +95,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(auth_utils, 'userHasValidSystemRoles').returns(true); + sinon.stub(authorization, 'userHasValidRole').returns(true); sinon.stub(search_queries, 'getSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); @@ -124,7 +125,7 @@ describe('search', () => { }, query: mockQuery }); - sinon.stub(auth_utils, 'userHasValidSystemRoles').returns(true); + sinon.stub(authorization, 'userHasValidRole').returns(true); sinon.stub(search_queries, 'getSpatialSearchResultsSQL').returns(SQL`something`); const result = search.getSearchResults(); diff --git a/api/src/paths/search.ts b/api/src/paths/search.ts index 3863897a88..43aa371a37 100644 --- a/api/src/paths/search.ts +++ b/api/src/paths/search.ts @@ -1,24 +1,34 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../constants/roles'; import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; +import { HTTP400 } from '../errors/custom-error'; import { searchResponseObject } from '../openapi/schemas/search'; +import { queries } from '../queries/queries'; +import { authorizeRequestHandler, userHasValidRole } from '../request-handlers/security/authorization'; import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; -import { getSpatialSearchResultsSQL } from '../queries/search-queries'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { userHasValidSystemRoles } from '../security/auth-utils'; const defaultLog = getLogger('paths/search'); -export const GET: Operation = [logRequest('paths/search', 'GET'), getSearchResults()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getSearchResults() +]; GET.apiDoc = { description: 'Gets a list of published project geometries for given systemUserId', tags: ['projects'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -57,9 +67,9 @@ export function getSearchResults(): RequestHandler { await connection.open(); const systemUserId = connection.systemUserId(); - const isUserAdmin = userHasValidSystemRoles([SYSTEM_ROLE.SYSTEM_ADMIN], req['system_user']['role_names']); + const isUserAdmin = userHasValidRole([SYSTEM_ROLE.SYSTEM_ADMIN], req['system_user']['role_names']); - const getSpatialSearchResultsSQLStatement = getSpatialSearchResultsSQL(isUserAdmin, systemUserId); + const getSpatialSearchResultsSQLStatement = queries.search.getSpatialSearchResultsSQL(isUserAdmin, systemUserId); if (!getSpatialSearchResultsSQLStatement) { throw new HTTP400('Failed to build SQL get statement'); diff --git a/api/src/paths/taxonomy/species/list.test.ts b/api/src/paths/taxonomy/species/list.test.ts new file mode 100644 index 0000000000..273c25d3ca --- /dev/null +++ b/api/src/paths/taxonomy/species/list.test.ts @@ -0,0 +1,97 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import { TaxonomyService } from '../../../services/taxonomy-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { GET, getSpeciesFromIds } from './list'; + +chai.use(sinonChai); + +describe('list', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('getSpeciesFromIds', () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns an empty array if no species ids are found', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const getSpeciesFromIdsStub = sinon.stub(TaxonomyService.prototype, 'getSpeciesFromIds').resolves([]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + ids: '' + }; + + const requestHandler = getSpeciesFromIds(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSpeciesFromIdsStub).to.have.been.calledWith([]); + + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ searchResponse: [] }); + }); + + it('returns an array of species', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const mock1 = ({ id: '1', label: 'something' } as unknown) as any; + const mock2 = ({ id: '2', label: 'anything' } as unknown) as any; + + const getSpeciesFromIdsStub = sinon.stub(TaxonomyService.prototype, 'getSpeciesFromIds').resolves([mock1, mock2]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + ids: '0=1&1=2' + }; + + const requestHandler = getSpeciesFromIds(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSpeciesFromIdsStub).to.have.been.calledWith(['1', '2']); + + expect(mockRes.jsonValue).to.eql({ searchResponse: [mock1, mock2] }); + expect(mockRes.statusValue).to.equal(200); + }); + + it('catches error, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(TaxonomyService.prototype, 'getSpeciesFromIds').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + ids: '0=1&1=2' + }; + + try { + const requestHandler = getSpeciesFromIds(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/taxonomy/species/list.ts b/api/src/paths/taxonomy/species/list.ts new file mode 100644 index 0000000000..518c69dade --- /dev/null +++ b/api/src/paths/taxonomy/species/list.ts @@ -0,0 +1,87 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import qs from 'qs'; +import { TaxonomyService } from '../../../services/taxonomy-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/taxonomy/list'); + +export const GET: Operation = [getSpeciesFromIds()]; + +GET.apiDoc = { + description: 'Gets the labels of the taxonomic units identified by the provided list of ids.', + tags: ['taxonomy'], + parameters: [ + { + description: 'Taxonomy ids.', + in: 'query', + name: 'ids', + required: true, + schema: { + type: 'string' + } + } + ], + responses: { + 200: { + description: 'Taxonomy search response object.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + searchResponse: { + type: 'array', + items: { + title: 'Species', + type: 'object', + required: ['id', 'label'], + properties: { + id: { + type: 'string' + }, + label: { + type: 'string' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get taxonomic search results. + * + * @returns {RequestHandler} + */ +export function getSpeciesFromIds(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'getSearchResults', message: 'request body', req_body: req.query }); + + const ids = Object.values(qs.parse(req.query.ids?.toString() || '')); + + try { + const taxonomyService = new TaxonomyService(); + const response = await taxonomyService.getSpeciesFromIds(ids as string[]); + + res.status(200).json({ searchResponse: response }); + } catch (error) { + defaultLog.error({ label: 'getSearchResults', message: 'error', error }); + throw error; + } + }; +} diff --git a/api/src/paths/taxonomy/species/search.test.ts b/api/src/paths/taxonomy/species/search.test.ts new file mode 100644 index 0000000000..0f4c9ccca1 --- /dev/null +++ b/api/src/paths/taxonomy/species/search.test.ts @@ -0,0 +1,97 @@ +import Ajv from 'ajv'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import { TaxonomyService } from '../../../services/taxonomy-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { GET, searchSpecies } from './search'; + +chai.use(sinonChai); + +describe('search', () => { + describe('openapi schema', () => { + const ajv = new Ajv(); + + it('is valid openapi v3 schema', () => { + expect(ajv.validateSchema((GET.apiDoc as unknown) as object)).to.be.true; + }); + }); + + describe('searchSpecies', () => { + afterEach(() => { + sinon.restore(); + }); + + it('returns an empty array if no species are found', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const getSpeciesFromIdsStub = sinon.stub(TaxonomyService.prototype, 'searchSpecies').resolves([]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + terms: '' + }; + + const requestHandler = searchSpecies(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSpeciesFromIdsStub).to.have.been.calledWith(''); + + expect(mockRes.statusValue).to.equal(200); + expect(mockRes.jsonValue).to.eql({ searchResponse: [] }); + }); + + it('returns an array of species', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const mock1 = ({ id: '1', label: 'something' } as unknown) as any; + const mock2 = ({ id: '2', label: 'anything' } as unknown) as any; + + const getSpeciesFromIdsStub = sinon.stub(TaxonomyService.prototype, 'searchSpecies').resolves([mock1, mock2]); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + terms: 't' + }; + + const requestHandler = searchSpecies(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(getSpeciesFromIdsStub).to.have.been.calledWith('t'); + + expect(mockRes.jsonValue).to.eql({ searchResponse: [mock1, mock2] }); + expect(mockRes.statusValue).to.equal(200); + }); + + it('catches error, and re-throws error', async () => { + const dbConnectionObj = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(TaxonomyService.prototype, 'searchSpecies').rejects(new Error('a test error')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + mockReq.query = { + ids: 'a' + }; + + try { + const requestHandler = searchSpecies(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('a test error'); + } + }); + }); +}); diff --git a/api/src/paths/taxonomy/species/search.ts b/api/src/paths/taxonomy/species/search.ts new file mode 100644 index 0000000000..707f933f20 --- /dev/null +++ b/api/src/paths/taxonomy/species/search.ts @@ -0,0 +1,85 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { TaxonomyService } from '../../../services/taxonomy-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/taxonomy/search'); + +export const GET: Operation = [searchSpecies()]; + +GET.apiDoc = { + description: 'Gets a list of taxonomic units.', + tags: ['taxonomy'], + parameters: [ + { + description: 'Taxonomy search parameters.', + in: 'query', + name: 'terms', + required: true, + schema: { + type: 'string' + } + } + ], + responses: { + 200: { + description: 'Taxonomy search response object.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + searchResponse: { + type: 'array', + items: { + title: 'Species', + type: 'object', + required: ['id', 'label'], + properties: { + id: { + type: 'string' + }, + label: { + type: 'string' + } + } + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get taxonomic search results. + * + * @returns {RequestHandler} + */ +export function searchSpecies(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'getSearchResults', message: 'request params', req_params: req.query.terms }); + + const term = String(req.query.terms) || ''; + try { + const taxonomySearch = new TaxonomyService(); + const response = await taxonomySearch.searchSpecies(term.toLowerCase()); + + res.status(200).json({ searchResponse: response }); + } catch (error) { + defaultLog.error({ label: 'getSearchResults', message: 'error', error }); + throw error; + } + }; +} diff --git a/api/src/paths/user.test.ts b/api/src/paths/user.test.ts deleted file mode 100644 index 8a70faa854..0000000000 --- a/api/src/paths/user.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as user from './user'; -import * as db from '../database/db'; -import * as user_queries from '../queries/users/user-queries'; -import { QueryResult } from 'pg'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../__mocks__/db'; - -chai.use(sinonChai); - -describe('user', () => { - const dbConnectionObj = getMockDBConnection(); - - describe('addSystemUser', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should throw a 400 error when no sql statement produced', async () => { - sinon.stub(user_queries, 'addSystemUserSQL').returns(null); - - try { - await user.addSystemUser('userIdentifier', 'identitySource', 10, { - ...dbConnectionObj, - systemUserId: () => { - return 10; - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 500 response when response has no rows', async () => { - sinon.stub(user_queries, 'addSystemUserSQL').returns(SQL`some query`); - - try { - await user.addSystemUser('userIdentifier', 'identitySource', 10, { - ...dbConnectionObj, - systemUserId: () => { - return 10; - }, - query: async () => { - return ({ - rows: null - } as unknown) as QueryResult; - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to add system user'); - } - }); - - it('should return the query rows result on success', async () => { - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = await user.addSystemUser('userIdentifier', 'identitySource', 10, { - ...dbConnectionObj, - systemUserId: () => { - return 10; - }, - query: async () => { - return { - rows: [ - { - id: 1, - uis_id: 'uis_id', - user_identifier: 'user_identifier', - record_effective_date: '2020/04/04' - } - ] - } as QueryResult; - } - }); - - expect(result.id).to.equal(1); - }); - }); - - describe('addUser', () => { - afterEach(() => { - sinon.restore(); - }); - - const sampleReq = { - keycloak_token: {}, - body: { - userIdentifier: 'uid', - identitySource: 'idsource' - } - }; - - let actualStatus: number = (null as unknown) as number; - - const sampleRes = { - send: (status: number) => { - actualStatus = status; - } - }; - - it('should throw a 400 error when no req body', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - try { - const result = user.addUser(); - - await result({ ...(sampleReq as any), body: null }, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: userIdentifier'); - } - }); - - it('should throw a 400 error when no userIdentifier', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - try { - const result = user.addUser(); - - await result( - { ...(sampleReq as any), body: { ...sampleReq.body, userIdentifier: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: userIdentifier'); - } - }); - - it('should throw a 400 error when no identitySource', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - try { - const result = user.addUser(); - - await result( - { ...(sampleReq as any), body: { ...sampleReq.body, identitySource: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: identitySource'); - } - }); - - it('should throw a 400 error when no system user id', async () => { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - try { - const result = user.addUser(); - - await result(sampleReq as any, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to identify system user ID'); - } - }); - - it('should throw a 400 error when no sql statement produced', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - sinon.stub(user_queries, 'addSystemUserSQL').returns(null); - - try { - const result = user.addUser(); - - await result(sampleReq as any, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw an error when a failure occurs', async () => { - const expectedError = new Error('cannot process query'); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - throw expectedError; - } - }); - - try { - const result = user.addUser(); - - await result(sampleReq as any, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); - } - }); - - it('should throw a 500 response when response has no rows', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return ({ - rows: null - } as unknown) as QueryResult; - } - }); - sinon.stub(user_queries, 'addSystemUserSQL').returns(SQL`some query`); - - try { - const result = user.addUser(); - - await result(sampleReq as any, sampleRes as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to add system user'); - } - }); - - it('should return status 200 on success', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return { - rows: [ - { - id: 1, - uis_id: 'uis_id', - user_identifier: 'user_identifier', - record_effective_date: '2020/04/04' - } - ] - } as QueryResult; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = user.addUser(); - - await result(sampleReq as any, sampleRes as any, (null as unknown) as any); - - expect(actualStatus).to.equal(200); - }); - }); -}); diff --git a/api/src/paths/user.ts b/api/src/paths/user.ts deleted file mode 100644 index fa7821522b..0000000000 --- a/api/src/paths/user.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getDBConnection, IDBConnection } from '../database/db'; -import { HTTP400, HTTP500 } from '../errors/CustomError'; -import { addSystemUserSQL } from '../queries/users/user-queries'; -import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; - -const defaultLog = getLogger('paths/user'); - -export const POST: Operation = [logRequest('paths/user', 'POST'), addUser()]; - -POST.apiDoc = { - description: 'Add a new system user.', - tags: ['user'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - requestBody: { - description: 'Add system user request object.', - content: { - 'application/json': { - schema: { - title: 'User Response Object', - type: 'object', - required: ['userIdentifier', 'identitySource'], - properties: { - userIdentifier: { - type: 'string' - }, - identitySource: { - type: 'string', - enum: ['idir', 'bceid'] - } - } - } - } - } - }, - responses: { - 200: { - description: 'Add system user OK.' - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -/** - * Add a system user by its user identifier. - * - * @returns {RequestHandler} - */ -export function addUser(): RequestHandler { - return async (req, res) => { - const connection = getDBConnection(req['keycloak_token']); - - const userIdentifier = req.body?.userIdentifier || null; - const identitySource = req.body?.identitySource || null; - - if (!userIdentifier) { - throw new HTTP400('Missing required body param: userIdentifier'); - } - - if (!identitySource) { - throw new HTTP400('Missing required body param: identitySource'); - } - - try { - await connection.open(); - - const systemUserId = connection.systemUserId(); - - if (!systemUserId) { - throw new HTTP400('Failed to identify system user ID'); - } - - const addSystemUserSQLStatement = addSystemUserSQL(userIdentifier, identitySource, systemUserId); - - if (!addSystemUserSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const response = await connection.query(addSystemUserSQLStatement.text, addSystemUserSQLStatement.values); - - await connection.commit(); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result) { - throw new HTTP500('Failed to add system user'); - } - - return res.send(200); - } catch (error) { - defaultLog.error({ label: 'getUser', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} - -/** - * Adds a new system user. - * - * Note: Does not account for the user already existing. - * - * @param {string} userIdentifier - * @param {string} identitySource - * @param {number} systemUserId - * @param {IDBConnection} connection - */ -export const addSystemUser = async ( - userIdentifier: string, - identitySource: string, - systemUserId: number, - connection: IDBConnection -) => { - const addSystemUserSQLStatement = addSystemUserSQL(userIdentifier, identitySource, systemUserId); - - if (!addSystemUserSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const response = await connection.query(addSystemUserSQLStatement.text, addSystemUserSQLStatement.values); - - const result = (response && response.rows && response.rows[0]) || null; - - if (!result) { - throw new HTTP500('Failed to add system user'); - } - - return result; -}; diff --git a/api/src/paths/user/add.test.ts b/api/src/paths/user/add.test.ts new file mode 100644 index 0000000000..594633bb58 --- /dev/null +++ b/api/src/paths/user/add.test.ts @@ -0,0 +1,143 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { SYSTEM_IDENTITY_SOURCE } from '../../constants/database'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import { UserObject } from '../../models/user'; +import { UserService } from '../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import * as user from './add'; + +chai.use(sinonChai); + +describe('user', () => { + describe('addSystemRoleUser', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no req body', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = undefined; + + try { + const requestHandler = user.addSystemRoleUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: userIdentifier'); + } + }); + + it('should throw a 400 error when no userIdentifier', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, + roleId: 1 + }; + + try { + const requestHandler = user.addSystemRoleUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: userIdentifier'); + } + }); + + it('should throw a 400 error when no identitySource', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + userIdentifier: 'username', + roleId: 1 + }; + + try { + const requestHandler = user.addSystemRoleUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: identitySource'); + } + }); + + it('should throw a 400 error when no roleId', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + userIdentifier: 'username', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR + }; + + try { + const requestHandler = user.addSystemRoleUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: roleId'); + } + }); + + it('adds a system user and returns 200 on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.body = { + userIdentifier: 'username', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, + roleId: 1 + }; + + const mockUserObject: UserObject = { + id: 1, + user_identifier: '', + record_end_date: '', + role_ids: [1], + role_names: [] + }; + + const ensureSystemUserStub = sinon.stub(UserService.prototype, 'ensureSystemUser').resolves(mockUserObject); + + const adduserSystemRolesStub = sinon.stub(UserService.prototype, 'addUserSystemRoles'); + + const requestHandler = user.addSystemRoleUser(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(ensureSystemUserStub).to.have.been.calledOnce; + expect(adduserSystemRolesStub).to.have.been.calledOnce; + }); + }); +}); diff --git a/api/src/paths/user/add.ts b/api/src/paths/user/add.ts new file mode 100644 index 0000000000..1da773b6bc --- /dev/null +++ b/api/src/paths/user/add.ts @@ -0,0 +1,130 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_IDENTITY_SOURCE } from '../../constants/database'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; +import { HTTP400 } from '../../errors/custom-error'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { UserService } from '../../services/user-service'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('paths/user/add'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + addSystemRoleUser() +]; + +POST.apiDoc = { + description: 'Add a new system user with role.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + requestBody: { + description: 'Add system user request object.', + content: { + 'application/json': { + schema: { + title: 'User Response Object', + type: 'object', + required: ['userIdentifier', 'identitySource', 'roleId'], + properties: { + userIdentifier: { + type: 'string' + }, + identitySource: { + type: 'string', + enum: [SYSTEM_IDENTITY_SOURCE.IDIR, SYSTEM_IDENTITY_SOURCE.BCEID] + }, + roleId: { + type: 'number', + minimum: 1 + } + } + } + } + } + }, + responses: { + 200: { + description: 'Add system user OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Add a system user by its user identifier and role. + * + * @returns {RequestHandler} + */ +export function addSystemRoleUser(): RequestHandler { + return async (req, res) => { + const connection = getDBConnection(req['keycloak_token']); + + const userIdentifier = req.body?.userIdentifier || null; + const identitySource = req.body?.identitySource || null; + + const roleId = req.body?.roleId || null; + + if (!userIdentifier) { + throw new HTTP400('Missing required body param: userIdentifier'); + } + + if (!identitySource) { + throw new HTTP400('Missing required body param: identitySource'); + } + + if (!roleId) { + throw new HTTP400('Missing required body param: roleId'); + } + + try { + await connection.open(); + + const userService = new UserService(connection); + + const userObject = await userService.ensureSystemUser(userIdentifier, identitySource); + + if (userObject) { + await userService.addUserSystemRoles(userObject.id, [roleId]); + } + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'getUser', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/user/list.test.ts b/api/src/paths/user/list.test.ts new file mode 100644 index 0000000000..96dc3372a5 --- /dev/null +++ b/api/src/paths/user/list.test.ts @@ -0,0 +1,44 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../database/db'; +import { UserService } from '../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import * as users from './list'; + +chai.use(sinonChai); + +describe('users', () => { + describe('getUserList', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return rows on success', async () => { + const mockDBConnection = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockResponse = [ + { + id: 1, + user_identifier: 'identifier', + record_end_date: '', + role_ids: [1, 2], + role_names: ['System Admin', 'Project Lead'] + } + ]; + + sinon.stub(UserService.prototype, 'listSystemUsers').resolves(mockResponse); + + const requestHandler = users.getUserList(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.jsonValue).to.eql(mockResponse); + }); + }); +}); diff --git a/api/src/paths/users.ts b/api/src/paths/user/list.ts similarity index 52% rename from api/src/paths/users.ts rename to api/src/paths/user/list.ts index 16de47e90e..615b691dd4 100644 --- a/api/src/paths/users.ts +++ b/api/src/paths/user/list.ts @@ -1,22 +1,33 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../constants/roles'; -import { getDBConnection } from '../database/db'; -import { HTTP400 } from '../errors/CustomError'; -import { getUserListSQL } from '../queries/users/user-queries'; -import { getLogger } from '../utils/logger'; -import { logRequest } from '../utils/path-utils'; +import { SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection } from '../../database/db'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { UserService } from '../../services/user-service'; +import { getLogger } from '../../utils/logger'; const defaultLog = getLogger('paths/user'); -export const GET: Operation = [logRequest('paths/user', 'GET'), getUserList()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + getUserList() +]; GET.apiDoc = { description: 'Get all Users.', tags: ['user'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -30,7 +41,24 @@ GET.apiDoc = { title: 'User Response Object', type: 'object', properties: { - // TODO needs finalizing (here and in the user-queries.ts SQL) + id: { + type: 'number' + }, + user_identifier: { + type: 'string' + }, + role_ids: { + type: 'array', + items: { + type: 'number' + } + }, + role_names: { + type: 'array', + items: { + type: 'string' + } + } } } } @@ -65,19 +93,15 @@ export function getUserList(): RequestHandler { const connection = getDBConnection(req['keycloak_token']); try { - const getUserListSQLStatement = getUserListSQL(); - - if (!getUserListSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - await connection.open(); - const getUserListResponse = await connection.query(getUserListSQLStatement.text, getUserListSQLStatement.values); + const userService = new UserService(connection); + + const response = await userService.listSystemUsers(); await connection.commit(); - return res.status(200).json(getUserListResponse && getUserListResponse.rows); + return res.status(200).json(response); } catch (error) { defaultLog.error({ label: 'getUserList', message: 'error', error }); throw error; diff --git a/api/src/paths/user/self.test.ts b/api/src/paths/user/self.test.ts index eeacb4504f..3f669d9c3d 100644 --- a/api/src/paths/user/self.test.ts +++ b/api/src/paths/user/self.test.ts @@ -2,12 +2,11 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as self from './self'; import * as db from '../../database/db'; -import * as user_queries from '../../queries/users/user-queries'; -import { QueryResult } from 'pg'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../__mocks__/db'; +import { HTTPError } from '../../errors/custom-error'; +import { UserService } from '../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import * as self from './self'; chai.use(sinonChai); @@ -16,97 +15,74 @@ describe('getUser', () => { sinon.restore(); }); - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {} - } as any; - - let actualResult = { - id: null, - user_identifier: null, - role_ids: null, - role_names: null - }; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - it('should throw a 400 error when no system user id', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); try { - const result = self.getUser(); + const requestHandler = self.getUser(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to identify system user ID'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to identify system user ID'); } }); it('should throw a 400 error when no sql statement produced', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - sinon.stub(user_queries, 'getUserByIdSQL').returns(null); + const dbConnectionObj = getMockDBConnection({ systemUserId: () => 1 }); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(UserService.prototype, 'getUserById').resolves(null); try { - const result = self.getUser(); + const requestHandler = self.getUser(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get system user'); } }); it('should return the user row on success', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return { - rowCount: 1, - rows: [ - { - id: 1, - user_identifier: 'identifier', - role_ids: [1, 2], - role_names: ['role 1', 'role 2'] - } - ] - } as QueryResult; - } - }); + const dbConnectionObj = getMockDBConnection({ systemUserId: () => 1 }); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'identifier', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); - const result = self.getUser(); + const requestHandler = self.getUser(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); - expect(actualResult.id).to.equal(1); - expect(actualResult.user_identifier).to.equal('identifier'); - expect(actualResult.role_ids).to.eql([1, 2]); - expect(actualResult.role_names).to.eql(['role 1', 'role 2']); + expect(mockRes.jsonValue.id).to.equal(1); + expect(mockRes.jsonValue.user_identifier).to.equal('identifier'); + expect(mockRes.jsonValue.role_ids).to.eql([1, 2]); + expect(mockRes.jsonValue.role_names).to.eql(['role 1', 'role 2']); }); it('should throw an error when a failure occurs', async () => { + const dbConnectionObj = getMockDBConnection({ systemUserId: () => 1 }); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + const expectedError = new Error('cannot process query'); sinon.stub(db, 'getDBConnection').returns({ @@ -117,35 +93,12 @@ describe('getUser', () => { }); try { - const result = self.getUser(); + const requestHandler = self.getUser(); - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + await requestHandler(mockReq, mockRes, mockNext); expect.fail(); } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); + expect((actualError as HTTPError).message).to.equal(expectedError.message); } }); - - it('should return null when response has no rowCount (no user found)', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return ({ - rowCount: 0, - rows: [] - } as unknown) as QueryResult; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = self.getUser(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.be.null; - }); }); diff --git a/api/src/paths/user/self.ts b/api/src/paths/user/self.ts index 1987206974..5b4f921e25 100644 --- a/api/src/paths/user/self.ts +++ b/api/src/paths/user/self.ts @@ -1,22 +1,32 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; import { getDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { getUserByIdSQL } from '../../queries/users/user-queries'; +import { HTTP400 } from '../../errors/custom-error'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; +import { UserService } from '../../services/user-service'; import { getLogger } from '../../utils/logger'; -import { logRequest } from '../../utils/path-utils'; const defaultLog = getLogger('paths/user/{userId}'); -export const GET: Operation = [logRequest('paths/user/{userId}', 'GET'), getUser()]; +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + discriminator: 'SystemUser' + } + ] + }; + }), + getUser() +]; GET.apiDoc = { description: 'Get user details for the currently authenticated user.', tags: ['user'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], responses: { @@ -27,8 +37,35 @@ GET.apiDoc = { schema: { title: 'User Response Object', type: 'object', + required: ['id', 'user_identifier', 'role_ids', 'role_names'], properties: { - // TODO needs finalizing (here and in the user-queries.ts SQL) + id: { + description: 'user id', + type: 'number' + }, + user_identifier: { + description: 'The unique user identifier', + type: 'string' + }, + record_end_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + description: 'Determines if the user record has expired', + nullable: true + }, + role_ids: { + description: 'list of role ids for the user', + type: 'array', + items: { + type: 'number' + } + }, + role_names: { + description: 'list of role names for the user', + type: 'array', + items: { + type: 'string' + } + } } } } @@ -64,27 +101,26 @@ export function getUser(): RequestHandler { try { await connection.open(); - const systemUserId = connection.systemUserId(); + const userId = connection.systemUserId(); - if (!systemUserId) { + if (!userId) { throw new HTTP400('Failed to identify system user ID'); } - const getUserSQLStatement = getUserByIdSQL(systemUserId); + const userService = new UserService(connection); - if (!getUserSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } + const userObject = await userService.getUserById(userId); - const response = await connection.query(getUserSQLStatement.text, getUserSQLStatement.values); + if (!userObject) { + throw new HTTP400('Failed to get system user'); + } await connection.commit(); - const result = (response && response.rows && response.rows[0]) || null; - - return res.status(200).json(result); + return res.status(200).json(userObject); } catch (error) { defaultLog.error({ label: 'getUser', message: 'error', error }); + await connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/user/{userId}/delete.test.ts b/api/src/paths/user/{userId}/delete.test.ts new file mode 100644 index 0000000000..6736ac6cbf --- /dev/null +++ b/api/src/paths/user/{userId}/delete.test.ts @@ -0,0 +1,700 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import project_participation_queries from '../../../queries/project-participation'; +import user_queries from '../../../queries/users'; +import { UserService } from '../../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import * as delete_endpoint from './delete'; + +chai.use(sinonChai); + +describe('removeSystemUser', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when missing required path param: userId', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: userId'); + } + }); + + it('should throw a 400 error when no sql statement returned from `getParticipantsFromAllSystemUsersProjectsSQL`', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(project_participation_queries, 'getParticipantsFromAllSystemUsersProjectsSQL').returns(null); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + } + }); + + it('should throw a 400 error if the user is the only Project Lead role on one or more projects', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '33' }; + mockReq.body = { roles: [1, 2] }; + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rowCount: 2, + rows: [ + { + project_participation_id: 47, + project_id: 3, + system_user_id: 33, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 57, + project_id: 1, + system_user_id: 33, + project_role_id: 3, + project_role_name: 'Viewer' + }, + { + project_participation_id: 40, + project_id: 1, + system_user_id: 27, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(project_participation_queries, 'getParticipantsFromAllSystemUsersProjectsSQL').returns(SQL`some query`); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Cannot remove user. User is the only Project Lead for one or more projects.' + ); + } + }); + + it('should throw a 400 error when it fails to get the system user', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves(null); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to get system user'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when user record has expired', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '2010-10-10', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('The system user is not active'); + } + }); + + it('should throw a 400 error when no sql statement returned for `deleteAllProjectRolesSql`', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + sinon.stub(user_queries, 'deleteAllProjectRolesSQL').returns(null); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build SQL delete statement for deleting project roles' + ); + } + }); + + it('should catch and re-throw an error if the database fails to delete all project roles', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + const expectedError = new Error('A database error'); + sinon.stub(delete_endpoint, 'deleteAllProjectRoles').rejects(expectedError); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(actualError).to.equal(expectedError); + } + }); + + it('should catch and re-throw an error if the database fails to delete all system roles', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + sinon.stub(delete_endpoint, 'deleteAllProjectRoles').resolves(); + + const expectedError = new Error('A database error'); + sinon.stub(UserService.prototype, 'deleteUserSystemRoles').rejects(expectedError); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(actualError).to.equal(expectedError); + } + }); + + it('should catch and re-throw an error if the database fails to deactivate the system user', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + sinon.stub(delete_endpoint, 'deleteAllProjectRoles').resolves(); + sinon.stub(UserService.prototype, 'deleteUserSystemRoles').resolves(); + + const expectedError = new Error('A database error'); + sinon.stub(UserService.prototype, 'deactivateSystemUser').rejects(expectedError); + + try { + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(actualError).to.equal(expectedError); + } + }); + + it('should return 200 on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { userId: '1' }; + mockReq.body = { roles: [1, 2] }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(delete_endpoint, 'checkIfUserIsOnlyProjectLeadOnAnyProject').resolves(); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'testname', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + sinon.stub(delete_endpoint, 'deleteAllProjectRoles').resolves(); + sinon.stub(UserService.prototype, 'deleteUserSystemRoles').resolves(); + sinon.stub(UserService.prototype, 'deactivateSystemUser').resolves(); + + const requestHandler = delete_endpoint.removeSystemUser(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); +}); + +describe('doAllProjectsHaveAProjectLeadIfUserIsRemoved', () => { + describe('user has Project Lead role', () => { + describe('user is on 1 project', () => { + it('should return false if the user is not the only Project Lead role', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(true); + }); + + it('should return true if the user is the only Project Lead role', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' // Only Project Lead on project 1 + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 2, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(false); + }); + }); + + describe('user is on multiple projects', () => { + it('should return true if the user is not the only Project Lead on all projects', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 1, + project_id: 2, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 2, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(true); + }); + + it('should return false if the user the only Project Lead on any project', () => { + const userId = 10; + + // User is on 1 project, and is not the only Project Lead + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 1, + project_id: 2, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Project Lead' // Only Project Lead on project 2 + }, + { + project_participation_id: 2, + project_id: 2, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(false); + }); + }); + }); + + describe('user does not have Project Lead role', () => { + describe('user is on 1 project', () => { + it('should return true', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Editor' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(true); + }); + }); + + describe('user is on multiple projects', () => { + it('should return true', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Editor' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 1, + project_id: 2, + system_user_id: userId, + project_role_id: 1, + project_role_name: 'Viewer' + }, + { + project_participation_id: 2, + project_id: 2, + system_user_id: 2, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(true); + }); + }); + }); + + describe('user is on no projects', () => { + it('should return false', () => { + const userId = 10; + + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: 20, + project_role_id: 1, + project_role_name: 'Editor' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 30, + project_role_id: 1, + project_role_name: 'Project Lead' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLeadIfUserIsRemoved(rows, userId); + + expect(result).to.equal(true); + }); + }); +}); + +describe('doAllProjectsHaveAProjectLead', () => { + it('should return false if no user has Project Lead role', () => { + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: 10, + project_role_id: 2, + project_role_name: 'Editor' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 2, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLead(rows); + + expect(result).to.equal(false); + }); + + it('should return true if one Project Lead role exists per project', () => { + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: 12, + project_role_id: 1, + project_role_name: 'Project Lead' // Only Project Lead on project 1 + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 2, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLead(rows); + + expect(result).to.equal(true); + }); + + it('should return true if one Project Lead exists on all projects', () => { + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: 10, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 2, + project_role_id: 2, + project_role_name: 'Editor' + }, + { + project_participation_id: 1, + project_id: 2, + system_user_id: 10, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 2, + system_user_id: 2, + project_role_id: 2, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLead(rows); + + expect(result).to.equal(true); + }); + + it('should return false if no Project Lead exists on any one project', () => { + const rows = [ + { + project_participation_id: 1, + project_id: 1, + system_user_id: 10, + project_role_id: 1, + project_role_name: 'Project Lead' + }, + { + project_participation_id: 2, + project_id: 1, + system_user_id: 20, + project_role_id: 2, + project_role_name: 'Editor' + }, + { + project_participation_id: 1, + project_id: 2, + system_user_id: 10, + project_role_id: 2, + project_role_name: 'Editor' + }, + { + project_participation_id: 2, + project_id: 2, + system_user_id: 20, + project_role_id: 2, + project_role_name: 'Editor' + } + ]; + + const result = delete_endpoint.doAllProjectsHaveAProjectLead(rows); + + expect(result).to.equal(false); + }); +}); diff --git a/api/src/paths/user/{userId}/delete.ts b/api/src/paths/user/{userId}/delete.ts new file mode 100644 index 0000000000..08ccb56674 --- /dev/null +++ b/api/src/paths/user/{userId}/delete.ts @@ -0,0 +1,250 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../../database/db'; +import { HTTP400 } from '../../../errors/custom-error'; +import { queries } from '../../../queries/queries'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { UserService } from '../../../services/user-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/delete'); + +export const DELETE: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + removeSystemUser() +]; + +DELETE.apiDoc = { + description: 'Remove a user from the system.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Remove system user from system OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function removeSystemUser(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ label: 'removeSystemUser', message: 'params', req_params: req.params }); + + const userId = (req.params && Number(req.params.userId)) || null; + + if (!userId) { + throw new HTTP400('Missing required path param: userId'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + await checkIfUserIsOnlyProjectLeadOnAnyProject(userId, connection); + + const userService = new UserService(connection); + + const usrObject = await userService.getUserById(userId); + + if (!usrObject) { + throw new HTTP400('Failed to get system user'); + } + + if (usrObject.record_end_date) { + throw new HTTP400('The system user is not active'); + } + + await deleteAllProjectRoles(userId, connection); + + await userService.deleteUserSystemRoles(userId); + + await userService.deactivateSystemUser(userId); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'removeSystemUser', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} + +export const checkIfUserIsOnlyProjectLeadOnAnyProject = async (userId: number, connection: IDBConnection) => { + const getAllParticipantsResponse = await getAllParticipantsFromSystemUsersProjects(userId, connection); + + // No projects associated to user, skip Project Lead role check + if (!getAllParticipantsResponse.length) { + return; + } + + const onlyProjectLeadResponse = doAllProjectsHaveAProjectLeadIfUserIsRemoved(getAllParticipantsResponse, userId); + + if (!onlyProjectLeadResponse) { + throw new HTTP400('Cannot remove user. User is the only Project Lead for one or more projects.'); + } +}; + +export const deleteAllProjectRoles = async (userId: number, connection: IDBConnection) => { + const sqlStatement = queries.users.deleteAllProjectRolesSQL(userId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL delete statement for deleting project roles'); + } + + connection.query(sqlStatement.text, sqlStatement.values); +}; + +/** + * collect all participants associated with user across all projects. + * + * @param {number} userId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getAllParticipantsFromSystemUsersProjects = async ( + userId: number, + connection: IDBConnection +): Promise => { + const getParticipantsFromAllSystemUsersProjectsSQLStatment = queries.projectParticipation.getParticipantsFromAllSystemUsersProjectsSQL( + userId + ); + + if (!getParticipantsFromAllSystemUsersProjectsSQLStatment) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await connection.query( + getParticipantsFromAllSystemUsersProjectsSQLStatment.text, + getParticipantsFromAllSystemUsersProjectsSQLStatment.values + ); + + return response.rows || []; +}; + +/** + * Given an array of project participation role objects, return false if any project has no Project Lead role. Return + * true otherwise. + * + * @param {any[]} rows + * @return {*} {boolean} + */ +export const doAllProjectsHaveAProjectLead = (rows: any[]): boolean => { + // No project with project lead + if (!rows.length) { + return false; + } + + const projectLeadsPerProject: { [key: string]: any } = {}; + + // count how many Project Lead roles there are per project + rows.forEach((row) => { + const key = row.project_id; + + if (!projectLeadsPerProject[key]) { + projectLeadsPerProject[key] = 0; + } + + if (row.project_role_name === PROJECT_ROLE.PROJECT_LEAD) { + projectLeadsPerProject[key] += 1; + } + }); + + const projectLeadCounts = Object.values(projectLeadsPerProject); + + // check if any projects would be left with no Project Lead + for (const count of projectLeadCounts) { + if (!count) { + // found a project with no Project Lead + return false; + } + } + + // all projects have a Project Lead + return true; +}; + +/** + * Given an array of project participation role objects, return true if any project has no Project Lead role after + * removing all rows associated with the provided `userId`. Return false otherwise. + * + * @param {any[]} rows + * @param {number} userId + * @return {*} {boolean} + */ +export const doAllProjectsHaveAProjectLeadIfUserIsRemoved = (rows: any[], userId: number): boolean => { + // No project with project lead + if (!rows.length) { + return false; + } + + const projectLeadsPerProject: { [key: string]: any } = {}; + + // count how many Project Lead roles there are per project + rows.forEach((row) => { + const key = row.project_id; + + if (!projectLeadsPerProject[key]) { + projectLeadsPerProject[key] = 0; + } + + if (row.system_user_id !== userId && row.project_role_name === PROJECT_ROLE.PROJECT_LEAD) { + projectLeadsPerProject[key] += 1; + } + }); + + const projectLeadCounts = Object.values(projectLeadsPerProject); + + // check if any projects would be left with no Project Lead + for (const count of projectLeadCounts) { + if (!count) { + // found a project with no Project Lead + return false; + } + } + + // all projects have a Project Lead + return true; +}; diff --git a/api/src/paths/user/{userId}/get.test.ts b/api/src/paths/user/{userId}/get.test.ts new file mode 100644 index 0000000000..bfdbea856a --- /dev/null +++ b/api/src/paths/user/{userId}/get.test.ts @@ -0,0 +1,97 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/custom-error'; +import { UserService } from '../../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import * as user from './get'; + +chai.use(sinonChai); + +describe('user', () => { + describe('getUserById', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no user Id is sent', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '' + }; + + try { + const requestHandler = user.getUserById(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param: userId'); + } + }); + + it('should throw a 400 error if it fails to get the system user', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + + sinon.stub(UserService.prototype, 'getUserById').resolves(null); + + try { + const requestHandler = user.getUserById(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get system user'); + } + }); + + it('finds user by Id and returns 200 and requestHandler on success', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'user_identifier', + record_end_date: '', + role_ids: [], + role_names: [] + }); + + const requestHandler = user.getUserById(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.jsonValue).to.eql({ + id: 1, + user_identifier: 'user_identifier', + record_end_date: '', + role_ids: [], + role_names: [] + }); + }); + }); +}); diff --git a/api/src/paths/user/{userId}/get.ts b/api/src/paths/user/{userId}/get.ts new file mode 100644 index 0000000000..9704014279 --- /dev/null +++ b/api/src/paths/user/{userId}/get.ts @@ -0,0 +1,140 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../constants/roles'; +import { getDBConnection } from '../../../database/db'; +import { HTTP400 } from '../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../request-handlers/security/authorization'; +import { UserService } from '../../../services/user-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/get'); + +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + getUserById() +]; + +GET.apiDoc = { + description: 'Get user details from userId.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'User details for userId.', + content: { + 'application/json': { + schema: { + title: 'User Response Object', + type: 'object', + properties: { + id: { + description: 'user id', + type: 'number' + }, + user_identifier: { + description: 'The unique user identifier', + type: 'string' + }, + record_end_date: { + oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }], + nullable: true, + description: 'Determines if the user record has expired' + }, + role_ids: { + description: 'list of role ids for the user', + type: 'array', + items: { + type: 'number' + } + }, + role_names: { + description: 'list of role names for the user', + type: 'array', + items: { + type: 'string' + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get a user by its user identifier. + * + * @returns {RequestHandler} + */ +export function getUserById(): RequestHandler { + return async (req, res) => { + if (!req.params.userId) { + throw new HTTP400('Missing required param: userId'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const userId = Number(req.params.userId); + + await connection.open(); + + const userService = new UserService(connection); + + const userObject = await userService.getUserById(userId); + + if (!userObject) { + throw new HTTP400('Failed to get system user'); + } + + await connection.commit(); + + return res.status(200).json(userObject); + } catch (error) { + defaultLog.error({ label: 'getUser', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/user/{userId}/projects/get.test.ts b/api/src/paths/user/{userId}/projects/get.test.ts new file mode 100644 index 0000000000..6f2d25a998 --- /dev/null +++ b/api/src/paths/user/{userId}/projects/get.test.ts @@ -0,0 +1,120 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import project_participation_queries from '../../../../queries/project-participation'; +import { getMockDBConnection } from '../../../../__mocks__/db'; +import * as projects from './get'; + +chai.use(sinonChai); + +describe('projects', () => { + const dbConnectionObj = getMockDBConnection(); + + describe('getAllUserProjects', () => { + afterEach(() => { + sinon.restore(); + }); + + const sampleReq = { + keycloak_token: {}, + params: { + userId: 1 + } + } as any; + + let actualResult: any = null; + + const sampleRes = { + status: () => { + return { + json: (result: any) => { + actualResult = result; + } + }; + } + }; + + it('should throw a 400 error when no params are sent', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = projects.getAllUserProjects(); + + await result({ ...(sampleReq as any), params: null }, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required params'); + } + }); + + it('should throw a 400 error when no user Id is sent', async () => { + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const result = projects.getAllUserProjects(); + + await result( + { ...(sampleReq as any), params: { ...sampleReq.params, userId: null } }, + (null as unknown) as any, + (null as unknown) as any + ); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required param: userId'); + } + }); + + it('should throw a 400 error when no sql statement returned for getProjectSQL', async () => { + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + sinon.stub(project_participation_queries, 'getAllUserProjectsSQL').returns(null); + + try { + const result = projects.getAllUserProjects(); + + await result(sampleReq, (null as unknown) as any, (null as unknown) as any); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + } + }); + + it('finds user by Id and returns 200 and result on success', async () => { + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rows: [{ project_id: 123, name: 'test', system_user_id: 12, project_role_id: 42, project_participation_id: 88 }] + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(project_participation_queries, 'getAllUserProjectsSQL').returns(SQL`something`); + + const result = projects.getAllUserProjects(); + + await result(sampleReq, sampleRes as any, (null as unknown) as any); + + expect(actualResult).to.eql([ + { project_id: 123, name: 'test', system_user_id: 12, project_role_id: 42, project_participation_id: 88 } + ]); + }); + }); +}); diff --git a/api/src/paths/user/{userId}/projects/get.ts b/api/src/paths/user/{userId}/projects/get.ts new file mode 100644 index 0000000000..91a16a76c8 --- /dev/null +++ b/api/src/paths/user/{userId}/projects/get.ts @@ -0,0 +1,173 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { queries } from '../../../../queries/queries'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { getLogger } from '../../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/projects/get'); +export const GET: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + getAllUserProjects() +]; + +GET.apiDoc = { + description: 'Gets a list of projects based on user Id.', + tags: ['projects'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + responses: { + 200: { + description: 'Projects response object for given user.', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + title: 'Project Get Response Object', + type: 'object', + properties: { + project_id: { + type: 'number' + }, + name: { + type: 'string' + }, + system_user_id: { + type: 'number' + }, + project_role_id: { + type: 'number' + }, + project_participation_id: { + type: 'number' + } + } + } + } + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get all projects (potentially based on filter criteria). + * + * @returns {RequestHandler} + */ +export function getAllUserProjects(): RequestHandler { + return async (req, res) => { + if (!req.params) { + throw new HTTP400('Missing required params'); + } + + if (!req.params.userId) { + throw new HTTP400('Missing required param: userId'); + } + + const connection = getDBConnection(req['keycloak_token']); + + try { + const userId = Number(req.params.userId); + + await connection.open(); + + const getAllUserProjectsSQLStatement = queries.projectParticipation.getAllUserProjectsSQL(userId); + + if (!getAllUserProjectsSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const getUserProjectsListResponse = await connection.query( + getAllUserProjectsSQLStatement.text, + getAllUserProjectsSQLStatement.values + ); + + await connection.commit(); + + let rows: any[] = []; + + if (getUserProjectsListResponse && getUserProjectsListResponse.rows) { + rows = getUserProjectsListResponse.rows; + } + + const result: any[] = _extractUserProjects(rows); + + return res.status(200).json(result); + } catch (error) { + defaultLog.error({ label: 'getAllUserProjects', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} + +/** + * Extract an array of project data from DB query. + * + * @export + * @param {any[]} rows DB query result rows + * @return {any[]} An array of project data + */ +export function _extractUserProjects(rows: any[]): any[] { + if (!rows || !rows.length) { + return []; + } + + const projects: any[] = []; + + rows.forEach((row) => { + const project: any = { + project_id: row.project_id, + name: row.name, + system_user_id: row.system_user_id, + project_role_id: row.project_role_id, + project_participation_id: row.project_participation_id + }; + + projects.push(project); + }); + + return projects; +} diff --git a/api/src/paths/user/{userId}/system-roles.test.ts b/api/src/paths/user/{userId}/system-roles.test.ts deleted file mode 100644 index 6212bb7ec3..0000000000 --- a/api/src/paths/user/{userId}/system-roles.test.ts +++ /dev/null @@ -1,385 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as system_roles from './system-roles'; -import * as db from '../../../database/db'; -import * as user_queries from '../../../queries/users/user-queries'; -import * as system_role_queries from '../../../queries/users/system-role-queries'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../../../__mocks__/db'; - -chai.use(sinonChai); - -describe('getAddSystemRolesHandler', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - params: { - userId: 1 - }, - body: { - roles: [1, 2] - } - } as any; - - let actualResult: number = (null as unknown) as number; - - const sampleRes = { - status: (status: number) => { - return { - send: () => { - actualResult = status; - } - }; - } - }; - - it('should throw a 400 error when missing required path param: userId', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = system_roles.getAddSystemRolesHandler(); - - await result( - { ...sampleReq, params: { ...sampleReq.params, userId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: userId'); - } - }); - - it('should throw a 400 error when missing roles in request body', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = system_roles.getAddSystemRolesHandler(); - - await result( - { ...sampleReq, body: { ...sampleReq.body, roles: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required body param: roles'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(null); - - try { - const result = system_roles.getAddSystemRolesHandler(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should throw a 400 error when no result or rowCount', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rows: [null] }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - try { - const result = system_roles.getAddSystemRolesHandler(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to get system user'); - } - }); - - it('should send a valid HTTP response on success (Filter out any system roles that have already been added to the user)', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: [{ id: 1, user_identifier: 'test name', role_ids: [1, 2], role_names: ['role 1', 'role 2'] }], - rowCount: 1 - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = system_roles.getAddSystemRolesHandler(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.equal(200); - }); - - it('should send a valid HTTP response on success (do not filter out any system roles that have already been added to the user)', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: [{ id: 1, user_identifier: 'test name', role_ids: [11, 22], role_names: ['role 11', 'role 22'] }], - rowCount: 1 - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - sinon.stub(system_roles, 'addSystemRoles'); - - const result = system_roles.getAddSystemRolesHandler(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.equal(200); - }); -}); - -describe('addSystemRoles', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const userId = 1; - const roles = [1, 2]; - - it('should throw a 400 error when it fails to postSystemRolesSQL', async () => { - sinon.stub(system_role_queries, 'postSystemRolesSQL').returns(null); - - try { - await system_roles.addSystemRoles(userId, roles, dbConnectionObj); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL insert statement'); - } - }); - - it('should throw a 400 error when it fails to add system roles (no result)', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves(null); - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, query: mockQuery }); - sinon.stub(system_role_queries, 'postSystemRolesSQL').returns(SQL`something`); - - try { - await system_roles.addSystemRoles(userId, roles, dbConnectionObj); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to add system roles'); - } - }); - - it('should throw a 400 error when it fails to add system roles (no rowCount)', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: null }); - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, query: mockQuery }); - sinon.stub(system_role_queries, 'postSystemRolesSQL').returns(SQL`something`); - - try { - await system_roles.addSystemRoles(userId, roles, dbConnectionObj); - - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to add system roles'); - } - }); -}); - -describe('removeSystemRoles', () => { - afterEach(() => { - sinon.restore(); - }); - - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {}, - params: { - userId: 1 - }, - query: { - roleId: ['1', '2'] - } - } as any; - - let actualResult: number = (null as unknown) as number; - - const sampleRes = { - status: (status: number) => { - return { - send: () => { - actualResult = status; - } - }; - } - }; - - it('should throw a 400 error when missing required path param: userId', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = system_roles.removeSystemRoles(); - - await result( - { ...sampleReq, params: { ...sampleReq.params, userId: null } }, - (null as unknown) as any, - (null as unknown) as any - ); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required path param: userId'); - } - }); - - it('should throw a 400 error when missing roles', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - try { - const result = system_roles.removeSystemRoles(); - - await result({ ...sampleReq, query: null }, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Missing required query param: roles'); - } - }); - - it('should throw a 400 error when no sql statement returned', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(system_role_queries, 'deleteSystemRolesSQL').returns(null); - - try { - const result = system_roles.removeSystemRoles(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL delete statement'); - } - }); - - it('should throw a 500 error when no result or rowCount', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: null }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(system_role_queries, 'deleteSystemRolesSQL').returns(SQL`some query`); - - try { - const result = system_roles.removeSystemRoles(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(500); - expect(actualError.message).to.equal('Failed to remove system roles'); - } - }); - - it('should send a valid HTTP response on success', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ rowCount: 1 }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - - sinon.stub(system_role_queries, 'deleteSystemRolesSQL').returns(SQL`some query`); - - const result = system_roles.removeSystemRoles(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.equal(200); - }); -}); diff --git a/api/src/paths/user/{userId}/system-roles.ts b/api/src/paths/user/{userId}/system-roles.ts deleted file mode 100644 index 63fe8598ea..0000000000 --- a/api/src/paths/user/{userId}/system-roles.ts +++ /dev/null @@ -1,260 +0,0 @@ -'use strict'; - -import { RequestHandler } from 'express'; -import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../constants/roles'; -import { getDBConnection, IDBConnection } from '../../../database/db'; -import { HTTP400, HTTP500 } from '../../../errors/CustomError'; -import { UserObject } from '../../../models/user'; -import { deleteSystemRolesSQL, postSystemRolesSQL } from '../../../queries/users/system-role-queries'; -import { getUserByIdSQL } from '../../../queries/users/user-queries'; -import { getLogger } from '../../../utils/logger'; -import { logRequest } from '../../../utils/path-utils'; - -const defaultLog = getLogger('paths/user/{userId}/system-roles'); - -export const POST: Operation = [logRequest('paths/user/{userId}/system-roles', 'POST'), getAddSystemRolesHandler()]; - -POST.apiDoc = { - description: 'Add system roles to a user.', - tags: ['user'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - parameters: [ - { - in: 'path', - name: 'userId', - schema: { - type: 'number' - }, - required: true - } - ], - requestBody: { - description: 'Add system roles to a user request object.', - content: { - 'application/json': { - schema: { - type: 'object', - required: ['roles'], - properties: { - roles: { - type: 'array', - items: { - type: 'number' - }, - description: 'An array of role ids' - } - } - } - } - } - }, - responses: { - 200: { - description: 'Add system user roles to user OK.' - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -export function getAddSystemRolesHandler(): RequestHandler { - return async (req, res) => { - defaultLog.debug({ label: 'addSystemRoles', message: 'params', req_params: req.params, req_body: req.body }); - - if (!req.params || !req.params.userId) { - throw new HTTP400('Missing required path param: userId'); - } - - if (!req.body || !req.body.roles || !req.body.roles.length) { - throw new HTTP400('Missing required body param: roles'); - } - - const userId = Number(req.params.userId); - const roles: number[] = req.body.roles; - const connection = getDBConnection(req['keycloak_token']); - - try { - await connection.open(); - - // Get the system user and their current roles - const getUserSQLStatement = getUserByIdSQL(userId); - - if (!getUserSQLStatement) { - throw new HTTP400('Failed to build SQL get statement'); - } - - const getUserResponse = await connection.query(getUserSQLStatement.text, getUserSQLStatement.values); - - const userResult = (getUserResponse && getUserResponse.rowCount && getUserResponse.rows[0]) || null; - - if (!userResult) { - throw new HTTP400('Failed to get system user'); - } - - const userObject = new UserObject(userResult); - - // Filter out any system roles that have already been added to the user - const rolesToAdd = roles.filter((role) => !userObject.role_ids.includes(role)); - - if (!rolesToAdd.length) { - // No new system roles to add, do nothing - return res.status(200).send(); - } - - await addSystemRoles(userId, roles, connection); - - await connection.commit(); - - return res.status(200).send(); - } catch (error) { - defaultLog.error({ label: 'addSystemRoles', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} - -/** - * Adds the specified roleIds to the user. - * - * Note: Does not account for any existing roles the user may already have. - * - * @param {number} userId - * @param {number[]} roleIds - * @param {IDBConnection} connection - */ -export const addSystemRoles = async (userId: number, roleIds: number[], connection: IDBConnection) => { - const postSystemRolesSqlStatement = postSystemRolesSQL(userId, roleIds); - - if (!postSystemRolesSqlStatement) { - throw new HTTP400('Failed to build SQL insert statement'); - } - - const postSystemRolesResponse = await connection.query( - postSystemRolesSqlStatement.text, - postSystemRolesSqlStatement.values - ); - - if (!postSystemRolesResponse || !postSystemRolesResponse.rowCount) { - throw new HTTP400('Failed to add system roles'); - } -}; - -export const DELETE: Operation = [logRequest('paths/user/{userId}/system-roles', 'DELETE'), removeSystemRoles()]; - -DELETE.apiDoc = { - description: 'Remove system roles from a user.', - tags: ['user'], - security: [ - { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] - } - ], - parameters: [ - { - in: 'path', - name: 'userId', - schema: { - type: 'number' - }, - required: true - }, - { - in: 'query', - name: 'roleId', - schema: { - type: 'array', - items: { - type: 'number' - } - }, - required: true - } - ], - responses: { - 200: { - description: 'Remove system user roles from user OK.' - }, - 400: { - $ref: '#/components/responses/400' - }, - 401: { - $ref: '#/components/responses/401' - }, - 403: { - $ref: '#/components/responses/401' - }, - 500: { - $ref: '#/components/responses/500' - }, - default: { - $ref: '#/components/responses/default' - } - } -}; - -export function removeSystemRoles(): RequestHandler { - return async (req, res) => { - defaultLog.debug({ label: 'removeSystemRoles', message: 'params', req_params: req.params, req_body: req.body }); - - const userId = (req.params && Number(req.params.userId)) || null; - - if (!userId) { - throw new HTTP400('Missing required path param: userId'); - } - - const roleIds: number[] = (req.query && (req.query.roleId as string[]).map((item: any) => Number(item))) || []; - - if (!roleIds.length) { - throw new HTTP400('Missing required query param: roles'); - } - - const connection = getDBConnection(req['keycloak_token']); - - try { - const sqlStatement = deleteSystemRolesSQL(userId, roleIds); - - if (!sqlStatement) { - throw new HTTP400('Failed to build SQL delete statement'); - } - - await connection.open(); - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - await connection.commit(); - - const result = (response && response.rowCount) || null; - - if (!result) { - throw new HTTP500('Failed to remove system roles'); - } - - return res.status(200).send(); - } catch (error) { - defaultLog.error({ label: 'removeSystemRoles', message: 'error', error }); - throw error; - } finally { - connection.release(); - } - }; -} diff --git a/api/src/paths/user/{userId}/system-roles/create.test.ts b/api/src/paths/user/{userId}/system-roles/create.test.ts new file mode 100644 index 0000000000..e8051d4626 --- /dev/null +++ b/api/src/paths/user/{userId}/system-roles/create.test.ts @@ -0,0 +1,216 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { UserService } from '../../../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import * as system_roles from './create'; + +chai.use(sinonChai); + +describe('getAddSystemRolesHandler', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when missing required path param: userId', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: userId'); + } + }); + + it('should throw a 400 error when missing roles in request body', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: null + }; + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + } + }); + + try { + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: roles'); + } + }); + + it('should throw a 400 error when no system user found', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(UserService.prototype, 'getUserById').resolves(null); + + try { + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get system user'); + } + }); + + it('re-throws the error thrown by UserService.addUserSystemRoles', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [11, 22], + role_names: ['role 11', 'role 22'] + }); + + sinon.stub(UserService.prototype, 'addUserSystemRoles').rejects(new Error('add user error')); + + try { + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('add user error'); + } + }); + + it('should send a 200 on success (when user has existing roles)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [1, 2], + role_names: ['role 1', 'role 2'] + }); + + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); + + it('should send a 200 on success (when user has no existing roles)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [], + role_names: ['role 11', 'role 22'] + }); + + sinon.stub(UserService.prototype, 'addUserSystemRoles').resolves(); + + const requestHandler = system_roles.getAddSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.statusValue).to.equal(200); + }); +}); diff --git a/api/src/paths/user/{userId}/system-roles/create.ts b/api/src/paths/user/{userId}/system-roles/create.ts new file mode 100644 index 0000000000..104ef7eacf --- /dev/null +++ b/api/src/paths/user/{userId}/system-roles/create.ts @@ -0,0 +1,138 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { UserService } from '../../../../services/user-service'; +import { getLogger } from '../../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/system-roles/create'); + +export const POST: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + getAddSystemRolesHandler() +]; + +POST.apiDoc = { + description: 'Add system roles to a user.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + description: 'Add system roles to a user request object.', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['roles'], + properties: { + roles: { + type: 'array', + items: { + type: 'number' + }, + description: 'An array of role ids' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Add system user roles to user OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function getAddSystemRolesHandler(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'getAddSystemRolesHandler', + message: 'params', + req_params: req.params, + req_body: req.body + }); + + if (!req.params || !req.params.userId) { + throw new HTTP400('Missing required path param: userId'); + } + + if (!req.body || !req.body.roles || !req.body.roles.length) { + throw new HTTP400('Missing required body param: roles'); + } + + const userId = Number(req.params.userId); + const roles: number[] = req.body.roles; + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const userService = new UserService(connection); + + const userObject = await userService.getUserById(userId); + + if (!userObject) { + throw new HTTP400('Failed to get system user'); + } + + // Filter out any system roles that have already been added to the user + const rolesToAdd = roles.filter((role) => !userObject.role_ids.includes(role)); + + if (!rolesToAdd.length) { + // No new system roles to add, do nothing + return res.status(200).send(); + } + + await userService.addUserSystemRoles(userId, roles); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'getAddSystemRolesHandler', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/user/{userId}/system-roles/update.test.ts b/api/src/paths/user/{userId}/system-roles/update.test.ts new file mode 100644 index 0000000000..9ad22c0b2a --- /dev/null +++ b/api/src/paths/user/{userId}/system-roles/update.test.ts @@ -0,0 +1,245 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../../database/db'; +import { HTTPError } from '../../../../errors/custom-error'; +import { UserService } from '../../../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../../__mocks__/db'; +import * as system_roles from './update'; + +chai.use(sinonChai); + +describe('updateSystemRolesHandler', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when missing required path param: userId', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required path param: userId'); + } + }); + + it('should throw a 400 error when missing roles in request body', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: null + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + try { + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing required body param: roles'); + } + }); + + it('should throw a 400 error when no system user found', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(UserService.prototype, 'getUserById').resolves(null); + + try { + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get system user'); + } + }); + + it('re-throws the error thrown by UserService.deleteUserSystemRoles', async () => { + const dbConnectionObj = getMockDBConnection(); + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [11, 22], + role_names: ['role 11', 'role 22'] + }); + + sinon.stub(UserService.prototype, 'deleteUserSystemRoles').rejects(new Error('a delete error')); + + try { + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('a delete error'); + } + }); + + it('re-throws the error thrown by UserService.addUserSystemRoles', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + const mockQuery = sinon.stub(); + + mockQuery.onCall(0).resolves({ rows: [], rowCount: 1 }); + mockQuery.onCall(1).resolves(null); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + systemUserId: () => { + return 20; + }, + query: mockQuery + }); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [11, 22], + role_names: ['role 11', 'role 22'] + }); + + sinon.stub(UserService.prototype, 'deleteUserSystemRoles').resolves(); + sinon.stub(UserService.prototype, 'addUserSystemRoles').rejects(new Error('an add error')); + + try { + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('an add error'); + } + }); + + it('should send a 200 on success (when user has existing roles)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); + + sinon.stub(UserService.prototype, 'getUserById').resolves({ + id: 1, + user_identifier: 'test name', + record_end_date: '', + role_ids: [11, 22], + role_names: ['role 1', 'role 2'] + }); + + const deleteUserSystemRolesStub = sinon.stub(UserService.prototype, 'deleteUserSystemRoles').resolves(); + sinon.stub(UserService.prototype, 'addUserSystemRoles').resolves(); + + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(deleteUserSystemRolesStub).to.have.been.calledOnce; + expect(mockRes.statusValue).to.equal(200); + }); + + it('should send a 200 on success (when user does not have existing roles)', async () => { + const dbConnectionObj = getMockDBConnection(); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + mockReq.params = { + userId: '1' + }; + mockReq.body = { + roles: [1] + }; + + const mockQuery = sinon.stub(); + + mockQuery.resolves({ + rowCount: 1 + }); + + sinon.stub(db, 'getDBConnection').returns({ + ...dbConnectionObj, + query: mockQuery + }); + + sinon + .stub(UserService.prototype, 'getUserById') + .resolves({ id: 1, user_identifier: 'test name', record_end_date: '', role_ids: [], role_names: [] }); + + const deleteUserSystemRolesStub = sinon.stub(UserService.prototype, 'deleteUserSystemRoles').resolves(); + sinon.stub(UserService.prototype, 'addUserSystemRoles').resolves(); + + const requestHandler = system_roles.updateSystemRolesHandler(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(deleteUserSystemRolesStub).not.to.have.been.called; + expect(mockRes.statusValue).to.equal(200); + }); +}); diff --git a/api/src/paths/user/{userId}/system-roles/update.ts b/api/src/paths/user/{userId}/system-roles/update.ts new file mode 100644 index 0000000000..58e8db61dc --- /dev/null +++ b/api/src/paths/user/{userId}/system-roles/update.ts @@ -0,0 +1,136 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { SYSTEM_ROLE } from '../../../../constants/roles'; +import { getDBConnection } from '../../../../database/db'; +import { HTTP400 } from '../../../../errors/custom-error'; +import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; +import { UserService } from '../../../../services/user-service'; +import { getLogger } from '../../../../utils/logger'; + +const defaultLog = getLogger('paths/user/{userId}/system-roles/update'); + +export const PATCH: Operation = [ + authorizeRequestHandler(() => { + return { + and: [ + { + validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN], + discriminator: 'SystemRole' + } + ] + }; + }), + updateSystemRolesHandler() +]; + +PATCH.apiDoc = { + description: 'Update system role for a user.', + tags: ['user'], + security: [ + { + Bearer: [] + } + ], + parameters: [ + { + in: 'path', + name: 'userId', + schema: { + type: 'number' + }, + required: true + } + ], + requestBody: { + description: 'Update system role for a user request object.', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['roles'], + properties: { + roles: { + type: 'array', + items: { + type: 'number' + }, + description: 'An array of role ids' + } + } + } + } + } + }, + responses: { + 200: { + description: 'Add system user roles to user OK.' + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/401' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +export function updateSystemRolesHandler(): RequestHandler { + return async (req, res) => { + defaultLog.debug({ + label: 'updateSystemRolesHandler', + message: 'params', + req_params: req.params, + req_body: req.body + }); + + if (!req.params || !req.params.userId) { + throw new HTTP400('Missing required path param: userId'); + } + + if (!req.body || !req.body.roles || !req.body.roles.length) { + throw new HTTP400('Missing required body param: roles'); + } + + const userId = Number(req.params.userId); + const roles: number[] = req.body.roles; + const connection = getDBConnection(req['keycloak_token']); + + try { + await connection.open(); + + const userService = new UserService(connection); + + const userObject = await userService.getUserById(userId); + + if (!userObject) { + throw new HTTP400('Failed to get system user'); + } + + if (userObject.role_ids.length) { + await userService.deleteUserSystemRoles(userId); + } + + //add new user system roles + await userService.addUserSystemRoles(userId, roles); + + await connection.commit(); + + return res.status(200).send(); + } catch (error) { + defaultLog.error({ label: 'updateSystemRolesHandler', message: 'error', error }); + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/users.test.ts b/api/src/paths/users.test.ts deleted file mode 100644 index 0369c5e985..0000000000 --- a/api/src/paths/users.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as users from './users'; -import * as db from '../database/db'; -import * as user_queries from '../queries/users/user-queries'; -import SQL from 'sql-template-strings'; -import { getMockDBConnection } from '../__mocks__/db'; - -chai.use(sinonChai); - -describe('users', () => { - const dbConnectionObj = getMockDBConnection(); - - const sampleReq = { - keycloak_token: {} - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - describe('getUserList', () => { - afterEach(() => { - sinon.restore(); - }); - - it('should throw a 400 error when fails to get sql statement', async () => { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - sinon.stub(user_queries, 'getUserListSQL').returns(null); - - try { - const result = users.getUserList(); - - await result(sampleReq, (null as unknown) as any, (null as unknown) as any); - expect.fail(); - } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); - } - }); - - it('should return rows on success', async () => { - const mockQuery = sinon.stub(); - - mockQuery.resolves({ - rows: [ - { - id: 1, - user_identifier: 'identifier', - role_ids: [1, 2], - role_name: ['System Admin', 'Project Lead'] - } - ] - }); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: mockQuery - }); - sinon.stub(user_queries, 'getUserListSQL').returns(SQL`something`); - - const result = users.getUserList(); - - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql([ - { - id: 1, - user_identifier: 'identifier', - role_ids: [1, 2], - role_name: ['System Admin', 'Project Lead'] - } - ]); - }); - }); -}); diff --git a/api/src/paths/version.ts b/api/src/paths/version.ts index 97c6386579..9c21e4ec90 100644 --- a/api/src/paths/version.ts +++ b/api/src/paths/version.ts @@ -16,11 +16,15 @@ GET.apiDoc = { properties: { version: { description: 'API Version', - type: 'number' + type: 'string' }, environment: { description: 'API Environment', type: 'string' + }, + timezone: { + description: 'API Timezone', + type: 'string' } } } diff --git a/api/src/paths/xlsx/transform.test.ts b/api/src/paths/xlsx/transform.test.ts index 98bc740f65..0c0dafeb7c 100644 --- a/api/src/paths/xlsx/transform.test.ts +++ b/api/src/paths/xlsx/transform.test.ts @@ -2,10 +2,7 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { getMockDBConnection } from '../../__mocks__/db'; import * as transform from './transform'; -import * as validate from './validate'; -import * as db from '../../database/db'; chai.use(sinonChai); @@ -47,60 +44,3 @@ describe('persistParseErrors', () => { expect(actualResult).to.eql({ status: 'failed', reason: 'Unable to parse submission' }); }); }); - -describe('getTransformationSchema', () => { - const sampleReq = { - keycloak_token: {}, - body: { - occurrence_submission_id: 1 - } - } as any; - - let actualResult: any = null; - - const sampleRes = { - status: () => { - return { - json: (result: any) => { - actualResult = result; - } - }; - } - }; - - const dbConnectionObj = getMockDBConnection(); - - afterEach(() => { - sinon.restore(); - }); - - it('should return with a failed status if no transformationSchema', async () => { - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => 20 }); - sinon.stub(validate, 'getTemplateMethodologySpecies').resolves({ - transform: null - }); - - const result = transform.getTransformationSchema(); - await result(sampleReq, sampleRes as any, (null as unknown) as any); - - expect(actualResult).to.eql({ - status: 'failed', - reason: 'Unable to fetch an appropriate transformation schema for your submission' - }); - }); - - it('should set the transformationSchema in the request and call next on success', async () => { - const nextSpy = sinon.spy(); - - sinon.stub(db, 'getDBConnection').returns({ ...dbConnectionObj, systemUserId: () => 20 }); - sinon.stub(validate, 'getTemplateMethodologySpecies').resolves({ - transform: 'transform' - }); - - const result = transform.getTransformationSchema(); - await result(sampleReq, (null as unknown) as any, nextSpy as any); - - expect(sampleReq.transformationSchema).to.eql('transform'); - expect(nextSpy).to.have.been.called; - }); -}); diff --git a/api/src/paths/xlsx/transform.ts b/api/src/paths/xlsx/transform.ts index 11dbfa0592..b69628c8ca 100644 --- a/api/src/paths/xlsx/transform.ts +++ b/api/src/paths/xlsx/transform.ts @@ -1,7 +1,7 @@ import AdmZip from 'adm-zip'; import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../constants/roles'; +import { PROJECT_ROLE } from '../../constants/roles'; import { SUBMISSION_STATUS_TYPE } from '../../constants/status'; import { getDBConnection } from '../../database/db'; import { @@ -12,18 +12,28 @@ import { sendResponse, updateSurveyOccurrenceSubmissionWithOutputKey } from '../../paths/dwc/validate'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; import { uploadBufferToS3 } from '../../utils/file-utils'; import { getLogger } from '../../utils/logger'; import { TransformationSchemaParser } from '../../utils/media/xlsx/transformation/transformation-schema-parser'; import { XLSXTransformation } from '../../utils/media/xlsx/transformation/xlsx-transformation'; import { XLSXCSV } from '../../utils/media/xlsx/xlsx-file'; -import { logRequest } from '../../utils/path-utils'; -import { getTemplateMethodologySpecies, prepXLSX } from './validate'; +import { getTemplateMethodologySpeciesRecord, prepXLSX } from './validate'; const defaultLog = getLogger('paths/xlsx/transform'); export const POST: Operation = [ - logRequest('paths/xlsx/transform', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), getOccurrenceSubmission(), getOccurrenceSubmissionInputS3Key(), getS3File(), @@ -41,7 +51,7 @@ POST.apiDoc = { tags: ['survey', 'observation', 'xlsx'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], requestBody: { @@ -50,8 +60,11 @@ POST.apiDoc = { 'application/json': { schema: { type: 'object', - required: ['occurrence_submission_id'], + required: ['project_id', 'occurrence_submission_id'], properties: { + project_id: { + type: 'number' + }, occurrence_submission_id: { description: 'A survey occurrence submission ID', type: 'number', @@ -123,11 +136,18 @@ export function getTransformationSchema(): RequestHandler { try { await connection.open(); - const templateMethodologySpeciesRecord = await getTemplateMethodologySpecies( - req.body.occurrence_submission_id, + const xlsxCsv = req['xlsx']; + const template_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_template_id; + const field_method_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_csm_id; + + const templateMethodologySpeciesRecord = await getTemplateMethodologySpeciesRecord( + Number(field_method_id), + Number(template_id), connection ); + await connection.commit(); + const transformationSchema = templateMethodologySpeciesRecord?.transform; if (!transformationSchema) { diff --git a/api/src/paths/xlsx/validate.test.ts b/api/src/paths/xlsx/validate.test.ts index de4e12fe93..89a7412836 100644 --- a/api/src/paths/xlsx/validate.test.ts +++ b/api/src/paths/xlsx/validate.test.ts @@ -2,12 +2,14 @@ import chai, { expect } from 'chai'; import { describe } from 'mocha'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import * as validate from './validate'; +import SQL from 'sql-template-strings'; +import xlsx from 'xlsx'; +import { HTTPError } from '../../errors/custom-error'; +import survey_queries from '../../queries/survey'; +import { ArchiveFile, MediaFile } from '../../utils/media/media-file'; import * as media_utils from '../../utils/media/media-utils'; -import * as survey_occurrence_queries from '../../queries/survey/survey-occurrence-queries'; -import { ArchiveFile } from '../../utils/media/media-file'; import { getMockDBConnection } from '../../__mocks__/db'; -import SQL from 'sql-template-strings'; +import * as validate from './validate'; chai.use(sinonChai); @@ -50,25 +52,97 @@ describe('prepXLSX', () => { expect(sampleReq.parseError).to.eql('Failed to parse submission, not a valid XLSX CSV file'); expect(nextSpy).to.have.been.called; }); + + it('should set parseError when no custom props set for the XLSX CSV file', async () => { + const nextSpy = sinon.spy(); + + const newWorkbook = xlsx.utils.book_new(); + + if (!newWorkbook.Custprops) { + newWorkbook.Custprops = {}; + } + + const ws_name = 'SheetJS'; + + /* make worksheet */ + const ws_data = [ + ['S', 'h', 'e', 'e', 't', 'J', 'S'], + [1, 2, 3, 4, 5] + ]; + const ws = xlsx.utils.aoa_to_sheet(ws_data); + + /* Add the worksheet to the workbook */ + xlsx.utils.book_append_sheet(newWorkbook, ws, ws_name); + + const buffer = xlsx.write(newWorkbook, { type: 'buffer' }); + + const mediaFile = new MediaFile('fileName', 'text/csv', buffer); + + sinon.stub(media_utils, 'parseUnknownMedia').returns(mediaFile); + + const requestHandler = validate.prepXLSX(); + await requestHandler(sampleReq, (null as unknown) as any, nextSpy as any); + + expect(sampleReq.parseError).to.eql('Failed to parse submission, template identification properties are missing'); + expect(nextSpy).to.have.been.called; + }); + + it('should call next when parameters are valid', async () => { + const nextSpy = sinon.spy(); + + const newWorkbook = xlsx.utils.book_new(); + + if (!newWorkbook.Custprops) { + newWorkbook.Custprops = {}; + } + newWorkbook.Custprops['sims_template_id'] = 1; + newWorkbook.Custprops['sims_csm_id'] = 1; + newWorkbook.Custprops['sims_species_id'] = 1234; + + const ws_name = 'SheetJS'; + + /* make worksheet */ + const ws_data = [ + ['S', 'h', 'e', 'e', 't', 'J', 'S'], + [1, 2, 3, 4, 5] + ]; + const ws = xlsx.utils.aoa_to_sheet(ws_data); + + /* Add the worksheet to the workbook */ + xlsx.utils.book_append_sheet(newWorkbook, ws, ws_name); + + const buffer = xlsx.write(newWorkbook, { type: 'buffer' }); + + const mediaFile = new MediaFile('fileName', 'text/csv', buffer); + + sinon.stub(media_utils, 'parseUnknownMedia').returns(mediaFile); + + const requestHandler = validate.prepXLSX(); + await requestHandler(sampleReq, (null as unknown) as any, nextSpy as any); + + expect(nextSpy).to.have.been.called; + }); }); -describe('getTemplateMethodologySpecies', () => { +describe('getTemplateMethodologySpeciesRecord', () => { afterEach(() => { sinon.restore(); }); const dbConnectionObj = getMockDBConnection(); - it('should throw 400 error when failed to build getTemplateMethodologySpeciesSQL statement', async () => { - sinon.stub(survey_occurrence_queries, 'getTemplateMethodologySpeciesSQL').returns(null); + it('should throw 400 error when failed to build getTemplateMethodologySpeciesRecordSQL statement', async () => { + sinon.stub(survey_queries, 'getTemplateMethodologySpeciesRecordSQL').returns(null); try { - await validate.getTemplateMethodologySpecies(1, { ...dbConnectionObj, systemUserId: () => 20 }); + await validate.getTemplateMethodologySpeciesRecord(1, 1, { ...dbConnectionObj, systemUserId: () => 20 }); expect.fail(); } catch (actualError) { - expect(actualError.status).to.equal(400); - expect(actualError.message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal( + 'Failed to build SQL get template methodology species record sql statement' + ); } }); @@ -79,11 +153,18 @@ describe('getTemplateMethodologySpecies', () => { rows: [null] }); - sinon.stub(survey_occurrence_queries, 'getTemplateMethodologySpeciesSQL').returns(SQL`something`); - - const result = await validate.getTemplateMethodologySpecies(1, { ...dbConnectionObj, systemUserId: () => 20 }); + sinon.stub(survey_queries, 'getTemplateMethodologySpeciesRecordSQL').returns(SQL`something`); - expect(result).to.equal(null); + try { + await validate.getTemplateMethodologySpeciesRecord(1, 1, { + ...dbConnectionObj, + systemUserId: () => 20 + }); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to query template methodology species table'); + } }); it('should return first row on success', async () => { @@ -97,9 +178,9 @@ describe('getTemplateMethodologySpecies', () => { ] }); - sinon.stub(survey_occurrence_queries, 'getTemplateMethodologySpeciesSQL').returns(SQL`something`); + sinon.stub(survey_queries, 'getTemplateMethodologySpeciesRecordSQL').returns(SQL`something`); - const result = await validate.getTemplateMethodologySpecies(1, { + const result = await validate.getTemplateMethodologySpeciesRecord(1, 1, { ...dbConnectionObj, query: mockQuery, systemUserId: () => 20 diff --git a/api/src/paths/xlsx/validate.ts b/api/src/paths/xlsx/validate.ts index eaa99e1f27..35a0680768 100644 --- a/api/src/paths/xlsx/validate.ts +++ b/api/src/paths/xlsx/validate.ts @@ -1,15 +1,16 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; +import { PROJECT_ROLE } from '../../constants/roles'; import { getDBConnection, IDBConnection } from '../../database/db'; -import { HTTP400 } from '../../errors/CustomError'; -import { getTemplateMethodologySpeciesSQL } from '../../queries/survey/survey-occurrence-queries'; +import { HTTP400 } from '../../errors/custom-error'; +import { queries } from '../../queries/queries'; +import { authorizeRequestHandler } from '../../request-handlers/security/authorization'; import { getLogger } from '../../utils/logger'; import { ICsvState } from '../../utils/media/csv/csv-file'; import { IMediaState, MediaFile } from '../../utils/media/media-file'; import { parseUnknownMedia } from '../../utils/media/media-utils'; import { ValidationSchemaParser } from '../../utils/media/validation/validation-schema-parser'; import { XLSXCSV } from '../../utils/media/xlsx/xlsx-file'; -import { logRequest } from '../../utils/path-utils'; import { getOccurrenceSubmission, getOccurrenceSubmissionInputS3Key, @@ -26,7 +27,17 @@ import { const defaultLog = getLogger('paths/xlsx/validate'); export const POST: Operation = [ - logRequest('paths/xlsx/validate', 'POST'), + authorizeRequestHandler((req) => { + return { + and: [ + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD, PROJECT_ROLE.PROJECT_EDITOR], + projectId: Number(req.body.project_id), + discriminator: 'ProjectRole' + } + ] + }; + }), getOccurrenceSubmission(), getOccurrenceSubmissionInputS3Key(), getS3File(), @@ -70,6 +81,14 @@ export function prepXLSX(): RequestHandler { const xlsxCsv = new XLSXCSV(parsedMedia); + const template_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_template_id; + const species_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_species_id; + const csm_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_csm_id; + + if (!template_id || !species_id || !csm_id) { + req['parseError'] = 'Failed to parse submission, template identification properties are missing'; + } + req['xlsx'] = xlsxCsv; next(); @@ -87,8 +106,13 @@ export function getValidationSchema(): RequestHandler { try { await connection.open(); - const templateMethodologySpeciesRecord = await getTemplateMethodologySpecies( - req.body.occurrence_submission_id, + const xlsxCsv = req['xlsx']; + const template_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_template_id; + const field_method_id = xlsxCsv.workbook.rawWorkbook.Custprops.sims_csm_id; + + const templateMethodologySpeciesRecord = await getTemplateMethodologySpeciesRecord( + Number(field_method_id), + Number(template_id), connection ); @@ -106,7 +130,7 @@ export function getValidationSchema(): RequestHandler { await insertSubmissionMessage( submissionStatusId, 'Error', - `Unable to fetch an appropriate validation schema for your submission`, + `Unable to fetch an appropriate template validation schema for your submission`, 'Missing Validation Schema', connection ); @@ -160,23 +184,28 @@ export function validateXLSX(): RequestHandler { } /** - * Get a template_methodology_species record from the template table. + * Get a template_methodology_species record from the template_methodologies_species table * - * @param {number} occurrenceSubmissionId + * @param {number} fieldMethodId + * @param {number} templateId * @param {IDBConnection} connection - * @return {*} {Promise} + * @return {*} {Promise} */ -export const getTemplateMethodologySpecies = async ( - occurrenceSubmissionId: number, +export const getTemplateMethodologySpeciesRecord = async ( + fieldMethodId: number, + templateId: number, connection: IDBConnection ): Promise => { - const sqlStatement = getTemplateMethodologySpeciesSQL(occurrenceSubmissionId); + const sqlStatement = queries.survey.getTemplateMethodologySpeciesRecordSQL(fieldMethodId, templateId); if (!sqlStatement) { - throw new HTTP400('Failed to build SQL get statement'); + throw new HTTP400('Failed to build SQL get template methodology species record sql statement'); } - const response = await connection.query(sqlStatement.text, sqlStatement.values); + if (!response) { + throw new HTTP400('Failed to query template methodology species table'); + } + return (response && response.rows && response.rows[0]) || null; }; diff --git a/api/src/queries/administrative-activity/administrative-activity-queries.test.ts b/api/src/queries/administrative-activity/administrative-activity-queries.test.ts index df6bf12582..4fd4e2ce3f 100644 --- a/api/src/queries/administrative-activity/administrative-activity-queries.test.ts +++ b/api/src/queries/administrative-activity/administrative-activity-queries.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../paths/administrative-activities'; import { countPendingAdministrativeActivitiesSQL, getAdministrativeActivitiesSQL, @@ -14,32 +15,32 @@ describe('getAdministrativeActivitiesSQL', () => { expect(response).to.not.be.null; }); - it('returns non null response when administrativeActivityStatusTypes is null and administrativeActivityStatusTypes is valid', () => { - const response = getAdministrativeActivitiesSQL((null as unknown) as string, ['status']); + it('returns non null response when administrativeActivityStatusTypes is undefined and administrativeActivityStatusTypes is provided', () => { + const response = getAdministrativeActivitiesSQL(undefined, ['status']); expect(response).to.not.be.null; }); - it('returns non null response when administrativeActivityStatusTypes is empty string and administrativeActivityStatusTypes is valid', () => { - const response = getAdministrativeActivitiesSQL('', ['status']); + it('returns non null response when administrativeActivityStatusTypes is empty and administrativeActivityStatusTypes is provided', () => { + const response = getAdministrativeActivitiesSQL([], ['status']); expect(response).to.not.be.null; }); - it('returns non null response when administrativeActivityStatusTypes is valid and administrativeActivityStatusTypes is null', () => { - const response = getAdministrativeActivitiesSQL('type', (null as unknown) as string[]); + it('returns non null response when administrativeActivityStatusTypes is provided and administrativeActivityStatusTypes is undefined', () => { + const response = getAdministrativeActivitiesSQL(['type'], undefined); expect(response).to.not.be.null; }); - it('returns non null response when administrativeActivityStatusTypes is valid and administrativeActivityStatusTypes is empty', () => { - const response = getAdministrativeActivitiesSQL('type', []); + it('returns non null response when administrativeActivityStatusTypes is provided and administrativeActivityStatusTypes is empty', () => { + const response = getAdministrativeActivitiesSQL(['type'], []); expect(response).to.not.be.null; }); - it('returns non null response when valid parameters provided', () => { - const response = getAdministrativeActivitiesSQL('type', ['status 1', 'status 2']); + it('returns non null response when allor parameters provided', () => { + const response = getAdministrativeActivitiesSQL(['type 1', 'type 2'], ['status 1', 'status 2']); expect(response).to.not.be.null; }); @@ -75,18 +76,8 @@ describe('countPendingAdministrativeActivitiesSQL', () => { }); describe('putAdministrativeActivitySQL', () => { - it('has a null administrativeActivityId', () => { - const response = putAdministrativeActivitySQL((null as unknown) as number, 1); - expect(response).to.be.null; - }); - - it('has a null administrativeActivityStatusTypeId', () => { - const response = putAdministrativeActivitySQL((null as unknown) as number, 1); - expect(response).to.be.null; - }); - - it('has valid parameters', () => { - const response = putAdministrativeActivitySQL(1, 1); + it('returns valid sql statement', () => { + const response = putAdministrativeActivitySQL(1, ADMINISTRATIVE_ACTIVITY_STATUS_TYPE.ACTIONED); expect(response).to.not.be.null; }); }); diff --git a/api/src/queries/administrative-activity/administrative-activity-queries.ts b/api/src/queries/administrative-activity/administrative-activity-queries.ts index 4df46a2899..7fc1a45100 100644 --- a/api/src/queries/administrative-activity/administrative-activity-queries.ts +++ b/api/src/queries/administrative-activity/administrative-activity-queries.ts @@ -1,25 +1,17 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/administrative-activity/administrative-activity-queries'); +import { ADMINISTRATIVE_ACTIVITY_STATUS_TYPE } from '../../paths/administrative-activities'; /** - * SQL query to get a list of administrative activities, optionally filtered by the administrative activity type name. + * SQL query to get a list of administrative activities. * - * @param {string} [administrativeActivityTypeName] + * @param {string[]} [administrativeActivityTypeNames] + * @param {string[]} [administrativeActivityStatusTypes] * @returns {SQLStatement} sql query object */ export const getAdministrativeActivitiesSQL = ( - administrativeActivityTypeName?: string, + administrativeActivityTypeNames?: string[], administrativeActivityStatusTypes?: string[] -): SQLStatement | null => { - defaultLog.debug({ - label: 'getAdministrativeActivitiesSQL', - message: 'params', - administrativeActivityTypeName, - administrativeActivityStatusTypes - }); - +): SQLStatement => { const sqlStatement = SQL` SELECT aa.administrative_activity_id as id, @@ -45,11 +37,21 @@ export const getAdministrativeActivitiesSQL = ( 1 = 1 `; - if (administrativeActivityTypeName) { + if (administrativeActivityTypeNames?.length) { sqlStatement.append(SQL` AND - aat.name = ${administrativeActivityTypeName} + aat.name IN ( `); + + // Add first element + sqlStatement.append(SQL`${administrativeActivityTypeNames[0]}`); + + for (let idx = 1; idx < administrativeActivityTypeNames.length; idx++) { + // Add subsequent elements, which get a comma prefix + sqlStatement.append(SQL`, ${administrativeActivityTypeNames[idx]}`); + } + + sqlStatement.append(SQL`)`); } if (administrativeActivityStatusTypes?.length) { @@ -71,13 +73,6 @@ export const getAdministrativeActivitiesSQL = ( sqlStatement.append(`;`); - defaultLog.debug({ - label: 'getAdministrativeActivitiesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -89,42 +84,26 @@ export const getAdministrativeActivitiesSQL = ( * @return {*} {(SQLStatement | null)} */ export const postAdministrativeActivitySQL = (systemUserId: number, data: unknown): SQLStatement | null => { - defaultLog.debug({ - label: 'postAdministrativeActivitySQL', - message: 'params', - systemUserId: systemUserId, - data: data - }); - - if (!systemUserId || !data) { + if (!systemUserId) { return null; } - const sqlStatement: SQLStatement = SQL` - INSERT INTO administrative_activity ( - reported_system_user_id, - administrative_activity_type_id, - administrative_activity_status_type_id, - data - ) VALUES ( - ${systemUserId}, - 1, - 1, - ${data} - ) - RETURNING - administrative_activity_id as id, - create_date::timestamptz - `; - - defaultLog.debug({ - label: 'postAdministrativeActivitySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + return SQL` + INSERT INTO administrative_activity ( + reported_system_user_id, + administrative_activity_type_id, + administrative_activity_status_type_id, + data + ) VALUES ( + ${systemUserId}, + 1, + 1, + ${data} + ) + RETURNING + administrative_activity_id as id, + create_date::timestamptz +`; }; /** @@ -134,13 +113,11 @@ export const postAdministrativeActivitySQL = (systemUserId: number, data: unknow * @return {*} {(SQLStatement | null)} */ export const countPendingAdministrativeActivitiesSQL = (userIdentifier: string): SQLStatement | null => { - defaultLog.debug({ label: 'countPendingAdministrativeActivitiesSQL', message: 'params', userIdentifier }); - if (!userIdentifier) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT * FROM administrative_activity aa @@ -148,60 +125,39 @@ export const countPendingAdministrativeActivitiesSQL = (userIdentifier: string): administrative_activity_status_type aast ON aa.administrative_activity_status_type_id = aast.administrative_activity_status_type_id - WHERE - (aa.data -> 'username')::text = '"' || ${userIdentifier} || '"' - AND aast.name = 'Pending'; - `; - - defaultLog.debug({ - label: 'countPendingAdministrativeActivitiesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + WHERE + (aa.data -> 'username')::text = '"' || ${userIdentifier} || '"' + AND aast.name = 'Pending'; +`; }; /** * SQL query update an existing administrative activity record. * * @param {number} administrativeActivityId - * @param {number} administrativeActivityStatusTypeId + * @param {ADMINISTRATIVE_ACTIVITY_STATUS_TYPE} administrativeActivityStatusTypeName * @return {*} {(SQLStatement | null)} */ export const putAdministrativeActivitySQL = ( administrativeActivityId: number, - administrativeActivityStatusTypeId: number -): SQLStatement | null => { - defaultLog.debug({ - label: 'putAdministrativeActivitySQL', - message: 'params', - administrativeActivityId, - administrativeActivityStatusTypeId - }); - - if (!administrativeActivityId || !administrativeActivityStatusTypeId) { - return null; - } + administrativeActivityStatusTypeName: ADMINISTRATIVE_ACTIVITY_STATUS_TYPE +): SQLStatement => { + return SQL` - const sqlStatement = SQL` UPDATE administrative_activity SET - administrative_activity_status_type_id = ${administrativeActivityStatusTypeId} + administrative_activity_status_type_id = ( + SELECT + administrative_activity_status_type_id + FROM + administrative_activity_status_type + WHERE + name = ${administrativeActivityStatusTypeName} + ) WHERE administrative_activity_id = ${administrativeActivityId} RETURNING administrative_activity_id as id; `; - - defaultLog.debug({ - label: 'putAdministrativeActivitySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; diff --git a/api/src/queries/administrative-activity/index.ts b/api/src/queries/administrative-activity/index.ts new file mode 100644 index 0000000000..e86a1fe640 --- /dev/null +++ b/api/src/queries/administrative-activity/index.ts @@ -0,0 +1,3 @@ +import * as administrativeActivity from './administrative-activity-queries'; + +export default { ...administrativeActivity }; diff --git a/api/src/queries/codes/code-queries.test.ts b/api/src/queries/codes/code-queries.test.ts index 524e7dc7c2..5c29050719 100644 --- a/api/src/queries/codes/code-queries.test.ts +++ b/api/src/queries/codes/code-queries.test.ts @@ -3,7 +3,6 @@ import { describe } from 'mocha'; import { getActivitySQL, getAdministrativeActivityStatusTypeSQL, - getCommonSurveyMethodologiesSQL, getFirstNationsSQL, getFundingSourceSQL, getInvestmentActionCategorySQL, @@ -13,8 +12,7 @@ import { getManagementActionTypeSQL, getProjectTypeSQL, getProprietorTypeSQL, - getSystemRolesSQL, - getTaxonsSQL + getSystemRolesSQL } from './code-queries'; describe('getManagementActionTypeSQL', () => { @@ -31,13 +29,6 @@ describe('getFirstNationsSQL', () => { }); }); -describe('getCommonSurveyMethodologiesSQL', () => { - it('returns valid sql statement', () => { - const response = getCommonSurveyMethodologiesSQL(); - expect(response).to.not.be.null; - }); -}); - describe('getFundingSourceSQL', () => { it('returns valid sql statement', () => { const response = getFundingSourceSQL(); @@ -107,10 +98,3 @@ describe('getAdministrativeActivityStatusTypeSQL', () => { expect(response).to.not.be.null; }); }); - -describe('getTaxonsSQL', () => { - it('returns valid sql statement', () => { - const response = getTaxonsSQL(); - expect(response).to.not.be.null; - }); -}); diff --git a/api/src/queries/codes/code-queries.ts b/api/src/queries/codes/code-queries.ts index 7542d84867..914fa61e79 100644 --- a/api/src/queries/codes/code-queries.ts +++ b/api/src/queries/codes/code-queries.ts @@ -6,7 +6,7 @@ import { SQL, SQLStatement } from 'sql-template-strings'; * @returns {SQLStatement} sql query object */ export const getManagementActionTypeSQL = (): SQLStatement => - SQL`SELECT management_action_type_id as id, name from management_action_type;`; + SQL`SELECT management_action_type_id as id, name from management_action_type where record_end_date is null;`; /** * SQL query to fetch first nation codes. @@ -14,7 +14,7 @@ export const getManagementActionTypeSQL = (): SQLStatement => * @returns {SQLStatement} sql query object */ export const getFirstNationsSQL = (): SQLStatement => - SQL`SELECT first_nations_id as id, name from first_nations ORDER BY name ASC;`; + SQL`SELECT first_nations_id as id, name from first_nations where record_end_date is null ORDER BY name ASC;`; /** * SQL query to fetch funding source codes. @@ -22,7 +22,7 @@ export const getFirstNationsSQL = (): SQLStatement => * @returns {SQLStatement} sql query object */ export const getFundingSourceSQL = (): SQLStatement => - SQL`SELECT funding_source_id as id, name from funding_source ORDER BY name ASC;`; + SQL`SELECT funding_source_id as id, name from funding_source where record_end_date is null ORDER BY name ASC;`; /** * SQL query to fetch proprietor type codes. @@ -30,29 +30,55 @@ export const getFundingSourceSQL = (): SQLStatement => * @returns {SQLStatement} sql query object */ export const getProprietorTypeSQL = (): SQLStatement => - SQL`SELECT proprietor_type_id as id, name from proprietor_type;`; + SQL`SELECT proprietor_type_id as id, name, is_first_nation from proprietor_type where record_end_date is null;`; /** * SQL query to fetch activity codes. * * @returns {SQLStatement} sql query object */ -export const getActivitySQL = (): SQLStatement => SQL`SELECT activity_id as id, name from activity;`; +export const getActivitySQL = (): SQLStatement => + SQL`SELECT activity_id as id, name from activity where record_end_date is null;`; /** - * SQL query to fetch common survey methodology codes. + * SQL query to fetch field method codes. * * @returns {SQLStatement} sql query object */ -export const getCommonSurveyMethodologiesSQL = (): SQLStatement => - SQL`SELECT common_survey_methodology_id as id, name from common_survey_methodology;`; +export const getFieldMethodsSQL = (): SQLStatement => + SQL`SELECT field_method_id as id, name, description from field_method where record_end_date is null;`; + +/** + * SQL query to fetch ecological season codes. + * + * @returns {SQLStatement} sql query object + */ +export const getEcologicalSeasonsSQL = (): SQLStatement => + SQL`SELECT ecological_season_id as id, name, description from ecological_season where record_end_date is null;`; + +/** + * SQL query to fetch vantage codes. + * + * @returns {SQLStatement} sql query object + */ +export const getVantageCodesSQL = (): SQLStatement => + SQL`SELECT vantage_id as id, name from vantage where record_end_date is null;`; + +/** + * SQL query to intended outcomes codes. + * + * @returns {SQLStatement} sql query object + */ +export const getIntendedOutcomesSQL = (): SQLStatement => + SQL`SELECT intended_outcome_id as id, name, description from intended_outcome where record_end_date is null;`; /** * SQL query to fetch project type codes. * * @returns {SQLStatement} sql query object */ -export const getProjectTypeSQL = (): SQLStatement => SQL`SELECT project_type_id as id, name from project_type;`; +export const getProjectTypeSQL = (): SQLStatement => + SQL`SELECT project_type_id as id, name from project_type where record_end_date is null;`; /** * SQL query to fetch investment action category codes. @@ -60,7 +86,7 @@ export const getProjectTypeSQL = (): SQLStatement => SQL`SELECT project_type_id * @returns {SQLStatement} sql query object */ export const getInvestmentActionCategorySQL = (): SQLStatement => - SQL`SELECT investment_action_category_id as id, funding_source_id as fs_id, name from investment_action_category ORDER BY name ASC;`; + SQL`SELECT investment_action_category_id as id, funding_source_id as fs_id, name from investment_action_category where record_end_date is null ORDER BY name ASC;`; /** * SQL query to fetch IUCN conservation action level 1 classification codes. @@ -68,7 +94,7 @@ export const getInvestmentActionCategorySQL = (): SQLStatement => * @returns {SQLStatement} sql query object */ export const getIUCNConservationActionLevel1ClassificationSQL = (): SQLStatement => - SQL`SELECT iucn_conservation_action_level_1_classification_id as id, name from iucn_conservation_action_level_1_classification;`; + SQL`SELECT iucn_conservation_action_level_1_classification_id as id, name from iucn_conservation_action_level_1_classification where record_end_date is null;`; /** * SQL query to fetch IUCN conservation action level 2 sub-classification codes. @@ -76,7 +102,7 @@ export const getIUCNConservationActionLevel1ClassificationSQL = (): SQLStatement * @returns {SQLStatement} sql query object */ export const getIUCNConservationActionLevel2SubclassificationSQL = (): SQLStatement => - SQL`SELECT iucn_conservation_action_level_2_subclassification_id as id, iucn_conservation_action_level_1_classification_id as iucn1_id, name from iucn_conservation_action_level_2_subclassification;`; + SQL`SELECT iucn_conservation_action_level_2_subclassification_id as id, iucn_conservation_action_level_1_classification_id as iucn1_id, name from iucn_conservation_action_level_2_subclassification where record_end_date is null;`; /** * SQL query to fetch IUCN conservation action level 3 sub-classification codes. @@ -84,34 +110,28 @@ export const getIUCNConservationActionLevel2SubclassificationSQL = (): SQLStatem * @returns {SQLStatement} sql query object */ export const getIUCNConservationActionLevel3SubclassificationSQL = (): SQLStatement => - SQL`SELECT iucn_conservation_action_level_3_subclassification_id as id, iucn_conservation_action_level_2_subclassification_id as iucn2_id, name from iucn_conservation_action_level_3_subclassification;`; + SQL`SELECT iucn_conservation_action_level_3_subclassification_id as id, iucn_conservation_action_level_2_subclassification_id as iucn2_id, name from iucn_conservation_action_level_3_subclassification where record_end_date is null;`; /** * SQL query to fetch system role codes. * * @returns {SQLStatement} sql query object */ -export const getSystemRolesSQL = (): SQLStatement => SQL`SELECT system_role_id as id, name from system_role;`; +export const getSystemRolesSQL = (): SQLStatement => + SQL`SELECT system_role_id as id, name from system_role where record_end_date is null;`; /** - * SQL query to fetch administrative activity status type codes. + * SQL query to fetch project role codes. * * @returns {SQLStatement} sql query object */ -export const getAdministrativeActivityStatusTypeSQL = (): SQLStatement => - SQL`SELECT administrative_activity_status_type_id as id, name FROM administrative_activity_status_type;`; +export const getProjectRolesSQL = (): SQLStatement => + SQL`SELECT project_role_id as id, name from project_role where record_end_date is null;`; /** - * SQL query to fetch taxon codes. + * SQL query to fetch administrative activity status type codes. * * @returns {SQLStatement} sql query object */ -export const getTaxonsSQL = (): SQLStatement => - SQL` - SELECT - wldtaxonomic_units_id as id, - CONCAT_WS(' - ', english_name, CONCAT_WS(' ', unit_name1, unit_name2, unit_name3)) as name - FROM - wldtaxonomic_units - WHERE - tty_name = 'SPECIES';`; +export const getAdministrativeActivityStatusTypeSQL = (): SQLStatement => + SQL`SELECT administrative_activity_status_type_id as id, name FROM administrative_activity_status_type where record_end_date is null;`; diff --git a/api/src/queries/codes/db-constant-queries.ts b/api/src/queries/codes/db-constant-queries.ts new file mode 100644 index 0000000000..8c21fb5f9c --- /dev/null +++ b/api/src/queries/codes/db-constant-queries.ts @@ -0,0 +1,13 @@ +import { SQL, SQLStatement } from 'sql-template-strings'; + +export const getDbCharacterSystemConstantSQL = (constantName: string): SQLStatement => + SQL`SELECT api_get_character_system_constant(${constantName}) as constant;`; + +export const getDbNumericSystemConstantSQL = (constantName: string): SQLStatement => + SQL`SELECT api_get_numeric_system_constant(${constantName}) as constant;`; + +export const getDbCharacterSystemMetaDataConstantSQL = (constantName: string): SQLStatement => + SQL`SELECT api_get_character_system_metadata_constant(${constantName}) as constant;`; + +export const getDbNumericSystemMetaDataConstantSQL = (constantName: string): SQLStatement => + SQL`SELECT api_get_numeric_system_metadata_constant(${constantName}) as constant;`; diff --git a/api/src/queries/codes/index.ts b/api/src/queries/codes/index.ts new file mode 100644 index 0000000000..51ad505a6c --- /dev/null +++ b/api/src/queries/codes/index.ts @@ -0,0 +1,4 @@ +import * as code from './code-queries'; +import * as dbConstant from './db-constant-queries'; + +export default { ...code, ...dbConstant }; diff --git a/api/src/queries/database/index.ts b/api/src/queries/database/index.ts new file mode 100644 index 0000000000..b596c80ee2 --- /dev/null +++ b/api/src/queries/database/index.ts @@ -0,0 +1,3 @@ +import * as userContext from './user-context-queries'; + +export default { ...userContext }; diff --git a/api/src/queries/user-context-queries.test.ts b/api/src/queries/database/user-context-queries.test.ts similarity index 91% rename from api/src/queries/user-context-queries.test.ts rename to api/src/queries/database/user-context-queries.test.ts index 64130e0ce0..d471a59498 100644 --- a/api/src/queries/user-context-queries.test.ts +++ b/api/src/queries/database/user-context-queries.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; +import { SYSTEM_IDENTITY_SOURCE } from '../../constants/database'; import { setSystemUserContextSQL } from './user-context-queries'; describe('setSystemUserContextSQL', () => { diff --git a/api/src/queries/database/user-context-queries.ts b/api/src/queries/database/user-context-queries.ts new file mode 100644 index 0000000000..f4b81f48a9 --- /dev/null +++ b/api/src/queries/database/user-context-queries.ts @@ -0,0 +1,13 @@ +import { SQL, SQLStatement } from 'sql-template-strings'; +import { SYSTEM_IDENTITY_SOURCE } from '../../constants/database'; + +export const setSystemUserContextSQL = ( + userIdentifier: string, + systemUserType: SYSTEM_IDENTITY_SOURCE +): SQLStatement | null => { + if (!userIdentifier) { + return null; + } + + return SQL`select api_set_context(${userIdentifier}, ${systemUserType});`; +}; diff --git a/api/src/queries/dwc/dwc-queries.ts b/api/src/queries/dwc/dwc-queries.ts new file mode 100644 index 0000000000..7c9f7d24f3 --- /dev/null +++ b/api/src/queries/dwc/dwc-queries.ts @@ -0,0 +1,458 @@ +import { SQL, SQLStatement } from 'sql-template-strings'; + +/** + * SQL query to get submission occurrence record given package ID for a particular survey. + * + * @param {number} dataPackageId + * @returns {SQLStatement} sql query object + */ +export const getSurveyOccurrenceSubmissionSQL = (dataPackageId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + SELECT + os.* + from + occurrence_submission os + , occurrence_submission_data_package osdp + where + osdp.data_package_id = ${dataPackageId} + and os.occurrence_submission_id = osdp.occurrence_submission_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get data package record given package ID. + * + * @param {number} dataPackageId + * @returns {SQLStatement} sql query object + */ +export const getDataPackageSQL = (dataPackageId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + SELECT + * + from + data_package + where + data_package_id = ${dataPackageId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get occurrence submission publish date. + * + * @param {number} occurrenceSubmissionId + * @returns {SQLStatement} sql query object + */ +export const getPublishedSurveyStatusSQL = (occurrenceSubmissionId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + SELECT + * + from + survey_status + where + survey_status = api_get_character_system_constant('OCCURRENCE_SUBMISSION_STATE_PUBLISHED') + and occurrence_submission_id = ${occurrenceSubmissionId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get survey data. + * + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const getSurveySQL = (surveyId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + SELECT + survey_id, + project_id, + field_method_id, + uuid, + name, + objectives, + start_date, + lead_first_name, + lead_last_name, + end_date, + location_description, + location_name, + publish_timestamp, + create_date, + create_user, + update_date, + update_user, + revision_count + from + survey + where survey_id = ${surveyId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + SELECT + project_id, + project_type_id, + uuid, + name, + objectives, + location_description, + start_date, + end_date, + caveats, + comments, + coordinator_first_name, + coordinator_last_name, + coordinator_email_address, + coordinator_agency_name, + coordinator_public, + publish_timestamp, + create_date, + create_user, + update_date, + update_user, + revision_count + from + project + where project_id = ${projectId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get survey funding source data. + * + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const getSurveyFundingSourceSQL = (surveyId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.*, + b.name investment_action_category_name, + c.name funding_source_name + from + project_funding_source a, + investment_action_category b, + funding_source c + where + project_funding_source_id in ( + select + project_funding_source_id + from + survey_funding_source + where + survey_id = ${surveyId}) + and b.investment_action_category_id = a.investment_action_category_id + and c.funding_source_id = b.funding_source_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project funding source data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectFundingSourceSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.*, + b.name investment_action_category_name, + c.name funding_source_name + from + project_funding_source a, + investment_action_category b, + funding_source c + where + project_id = ${projectId} + and b.investment_action_category_id = a.investment_action_category_id + and c.funding_source_id = b.funding_source_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get geometry bounding box. + * + * @param {number} primaryKey + * @param {string} primaryKeyName + * @param {string} targetTable + * @returns {SQLStatement} sql query object + */ +export const getGeometryBoundingBoxSQL = ( + primaryKey: number, + primaryKeyName: string, + targetTable: string +): SQLStatement => { + // TODO: this only provides us with the bounding box of the first polygon + const sqlStatement: SQLStatement = SQL` + with envelope as ( + select + ST_Envelope(geography::geometry) geom + from ` + .append(targetTable) + .append( + SQL` + where ` + ) + .append(primaryKeyName).append(SQL` = ${primaryKey}) + select + st_xmax(geom), + st_ymax(geom), + st_xmin(geom), + st_ymin(geom) + from + envelope; + `); + + return sqlStatement; +}; + +/** + * SQL query to get geometry polygons. + * + * @param {number} primaryKey + * @param {string} primaryKeyName + * @param {string} targetTable + * @returns {SQLStatement} sql query object + */ +export const getGeometryPolygonsSQL = ( + primaryKey: number, + primaryKeyName: string, + targetTable: string +): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + with polygons as ( + select + (st_dumppoints(g.geom)).* + from ( + select + geography::geometry as geom + from ` + .append(targetTable) + .append( + SQL` + where ` + ) + .append(primaryKeyName).append(SQL` = ${primaryKey}) as g), + points as ( + select + path[1] polygon, + path[2] point, + jsonb_build_array(st_y(p.geom), st_x(p.geom)) points + from + polygons p + order by + path[1], + path[2]) + select + json_agg(p.points) points + from + points p + group by + polygon; + `); + + return sqlStatement; +}; + +/** + * SQL query to get taxonomic coverage. + * + * @param {number} surveyId + * @param {boolean} isFocal + * @returns {SQLStatement} sql query object + */ +export const getTaxonomicCoverageSQL = (surveyId: number, isFocal: boolean): SQLStatement => { + let focalPredicate = 'and b.is_focal'; + if (!isFocal) { + focalPredicate = 'and not b.is_focal'; + } + + // TODO replace call to wldtaxonomic_units with a call to the taxonomy service + const sqlStatement: SQLStatement = SQL` + select + a.* + from + wldtaxonomic_units a, + study_species b + where + a.wldtaxonomic_units_id = b.wldtaxonomic_units_id + and b.survey_id = ${surveyId} + `.append(focalPredicate); + + return sqlStatement; +}; + +/** + * SQL query to get project IUCN conservation data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectIucnConservationSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name level_1_name, + b.name level_2_name, + c.name level_3_name + from + iucn_conservation_action_level_1_classification a, + iucn_conservation_action_level_2_subclassification b, + iucn_conservation_action_level_3_subclassification c, + project_iucn_action_classification d + where + d.project_id = ${projectId} + and c.iucn_conservation_action_level_3_subclassification_id = d.iucn_conservation_action_level_3_subclassification_id + and b.iucn_conservation_action_level_2_subclassification_id = c.iucn_conservation_action_level_2_subclassification_id + and a.iucn_conservation_action_level_1_classification_id = b.iucn_conservation_action_level_1_classification_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project stakeholder partnership data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectStakeholderPartnershipSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name + from + stakeholder_partnership a + where + a.project_id = ${projectId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project activity data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectActivitySQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name + from + activity a, + project_activity b + where + b.project_id = ${projectId} + and a.activity_id = b.activity_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get climate initiative data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectClimateInitiativeSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name + from + climate_change_initiative a, + project_climate_initiative b + where + b.project_id = ${projectId} + and a.climate_change_initiative_id = b.climate_change_initiative_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project first nations data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectFirstNationsSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name + from + first_nations a, + project_first_nation b + where + b.project_id = ${projectId} + and a.first_nations_id = b.first_nations_id; + `; + + return sqlStatement; +}; + +/** + * SQL query to get project management actions data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getProjectManagementActionsSQL = (projectId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.* + from + management_action_type a, + project_management_actions b + where + a.management_action_type_id = b.management_action_type_id + and b.project_id = ${projectId}; + `; + + return sqlStatement; +}; + +/** + * SQL query to get survey proprietor data. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getSurveyProprietorSQL = (surveyId: number): SQLStatement => { + const sqlStatement: SQLStatement = SQL` + select + a.name proprietor_type_name, + b.name first_nations_name, + c.* + from + proprietor_type a, + first_nations b, + survey_proprietor c + where + c.survey_id = ${surveyId} + and b.first_nations_id = c.first_nations_id + and a.proprietor_type_id = c.proprietor_type_id; + `; + + return sqlStatement; +}; diff --git a/api/src/queries/dwc/index.ts b/api/src/queries/dwc/index.ts new file mode 100644 index 0000000000..87a461dc6b --- /dev/null +++ b/api/src/queries/dwc/index.ts @@ -0,0 +1,3 @@ +import * as dwc from './dwc-queries'; + +export default { ...dwc }; diff --git a/api/src/queries/occurrence/index.ts b/api/src/queries/occurrence/index.ts new file mode 100644 index 0000000000..27d7393a03 --- /dev/null +++ b/api/src/queries/occurrence/index.ts @@ -0,0 +1,4 @@ +import * as occurrenceCreate from './occurrence-create-queries'; +import * as occurrenceView from './occurrence-view-queries'; + +export default { ...occurrenceCreate, ...occurrenceView }; diff --git a/api/src/queries/occurrence/occurrence-create-queries.ts b/api/src/queries/occurrence/occurrence-create-queries.ts index b184f73c91..9434791103 100644 --- a/api/src/queries/occurrence/occurrence-create-queries.ts +++ b/api/src/queries/occurrence/occurrence-create-queries.ts @@ -1,18 +1,8 @@ import { SQL, SQLStatement } from 'sql-template-strings'; import { PostOccurrence } from '../../models/occurrence-create'; -import { getLogger } from '../../utils/logger'; -import { parseUTMString } from '../../utils/spatial-utils'; - -const defaultLog = getLogger('queries/occurrence/occurrence-create-queries'); +import { parseLatLongString, parseUTMString } from '../../utils/spatial-utils'; export const postOccurrenceSQL = (occurrenceSubmissionId: number, occurrence: PostOccurrence): SQLStatement | null => { - defaultLog.debug({ - label: 'postOccurrenceSQL', - message: 'params', - occurrenceSubmissionId, - occurrence - }); - if (!occurrenceSubmissionId || !occurrence) { return null; } @@ -44,8 +34,10 @@ export const postOccurrenceSQL = (occurrenceSubmissionId: number, occurrence: Po `; const utm = parseUTMString(occurrence.verbatimCoordinates); + const latLong = parseLatLongString(occurrence.verbatimCoordinates); if (utm) { + // transform utm string into point, if it is not null sqlStatement.append(SQL` ,public.ST_Transform( public.ST_SetSRID( @@ -55,20 +47,25 @@ export const postOccurrenceSQL = (occurrenceSubmissionId: number, occurrence: Po 4326 ) `); - } else { + } else if (latLong) { + // transform latLong string into point, if it is not null sqlStatement.append(SQL` - ,null + ,public.ST_Transform( + public.ST_SetSRID( + public.ST_MakePoint(${latLong.long}, ${latLong.lat}), + 4326 + ), + 4326 + ) `); + } else { + // insert null geography + sqlStatement.append(SQL` + ,null + `); } sqlStatement.append(');'); - defaultLog.debug({ - label: 'postOccurrenceSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/occurrence/occurrence-view-queries.ts b/api/src/queries/occurrence/occurrence-view-queries.ts index a4c4e74dc7..7c77743c2c 100644 --- a/api/src/queries/occurrence/occurrence-view-queries.ts +++ b/api/src/queries/occurrence/occurrence-view-queries.ts @@ -1,15 +1,6 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/occurrence/occurrence-view-queries'); export const getOccurrencesForViewSQL = (occurrenceSubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getOccurrencesForViewSQL', - message: 'params', - occurrenceSubmissionId - }); - if (!occurrenceSubmissionId) { return null; } @@ -38,12 +29,5 @@ export const getOccurrencesForViewSQL = (occurrenceSubmissionId: number): SQLSta os.delete_timestamp is null; `; - defaultLog.debug({ - label: 'getOccurrencesForViewSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/permit/index.ts b/api/src/queries/permit/index.ts new file mode 100644 index 0000000000..639ee81258 --- /dev/null +++ b/api/src/queries/permit/index.ts @@ -0,0 +1,5 @@ +import * as permitCreate from './permit-create-queries'; +import * as permitUpdate from './permit-update-queries'; +import * as permitView from './permit-view-queries'; + +export default { ...permitCreate, ...permitUpdate, ...permitView }; diff --git a/api/src/queries/permit/permit-create-queries.test.ts b/api/src/queries/permit/permit-create-queries.test.ts index e2aecb1d65..16594109fb 100644 --- a/api/src/queries/permit/permit-create-queries.test.ts +++ b/api/src/queries/permit/permit-create-queries.test.ts @@ -62,7 +62,7 @@ describe('postProjectPermitSQL', () => { }); it('returns null when no system user id', () => { - const response = postProjectPermitSQL('123', 'type', 1, null); + const response = postProjectPermitSQL('123', 'type', 1, (null as unknown) as number); expect(response).to.be.null; }); diff --git a/api/src/queries/permit/permit-create-queries.ts b/api/src/queries/permit/permit-create-queries.ts index ff51a7e673..21d8937e66 100644 --- a/api/src/queries/permit/permit-create-queries.ts +++ b/api/src/queries/permit/permit-create-queries.ts @@ -1,9 +1,6 @@ import { SQL, SQLStatement } from 'sql-template-strings'; import { IPostPermitNoSampling } from '../../models/permit-no-sampling'; import { PostCoordinatorData } from '../../models/project-create'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/permit/permit-create-queries'); /** * SQL query to insert a permit row for permit associated to a project. @@ -11,24 +8,15 @@ const defaultLog = getLogger('queries/permit/permit-create-queries'); * @param {string} permitNumber * @param {string} permitType * @param {number} projectId - * @param {number | null} systemUserId + * @param {number} systemUserId * @returns {SQLStatement} sql query object */ export const postProjectPermitSQL = ( permitNumber: string, permitType: string, projectId: number, - systemUserId: number | null + systemUserId: number ): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectPermitSQL', - message: 'params', - permitNumber, - permitType, - projectId, - systemUserId - }); - if (!permitNumber || !permitType || !projectId || !systemUserId) { return null; } @@ -49,13 +37,6 @@ export const postProjectPermitSQL = ( permit_id as id; `; - defaultLog.debug({ - label: 'postProjectPermitSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -70,13 +51,6 @@ export const postPermitNoSamplingSQL = ( noSamplePermit: IPostPermitNoSampling & PostCoordinatorData, systemUserId: number | null ): SQLStatement | null => { - defaultLog.debug({ - label: 'postPermitNoSamplingSQL', - message: 'params', - noSamplePermit, - systemUserId - }); - if (!noSamplePermit || !systemUserId) { return null; } @@ -103,12 +77,5 @@ export const postPermitNoSamplingSQL = ( permit_id as id; `; - defaultLog.debug({ - label: 'postPermitNoSamplingSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/permit/permit-update-queries.ts b/api/src/queries/permit/permit-update-queries.ts index ac1c747a1c..5bb02d3aa5 100644 --- a/api/src/queries/permit/permit-update-queries.ts +++ b/api/src/queries/permit/permit-update-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/permit/permit-update-queries'); /** * SQL query to associate existing non-sampling permits to a project @@ -11,18 +8,11 @@ const defaultLog = getLogger('queries/permit/permit-update-queries'); * @returns {SQLStatement} sql query object */ export const associatePermitToProjectSQL = (permitId: number, projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'associatePermitToProjectSQL', - message: 'params', - permitId, - projectId - }); - if (!permitId || !projectId) { return null; } - const sqlStatement = SQL` + return SQL` UPDATE permit SET project_id = ${projectId}, @@ -33,13 +23,4 @@ export const associatePermitToProjectSQL = (permitId: number, projectId: number) WHERE permit_id = ${permitId}; `; - - defaultLog.debug({ - label: 'associatePermitToProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; diff --git a/api/src/queries/permit/permit-view-queries.test.ts b/api/src/queries/permit/permit-view-queries.test.ts index 0fb961964a..f57a5d822f 100644 --- a/api/src/queries/permit/permit-view-queries.test.ts +++ b/api/src/queries/permit/permit-view-queries.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { getNonSamplingPermitsSQL, getAllPermitsSQL } from './permit-view-queries'; +import { getAllPermitsSQL, getNonSamplingPermitsSQL } from './permit-view-queries'; describe('getNonSamplingPermitsSQL', () => { it('returns null when no system user id', () => { diff --git a/api/src/queries/permit/permit-view-queries.ts b/api/src/queries/permit/permit-view-queries.ts index ab95ddf7e9..c90d8a701d 100644 --- a/api/src/queries/permit/permit-view-queries.ts +++ b/api/src/queries/permit/permit-view-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/permit/permit-view-queries'); /** * SQL query to get all non-sampling permits @@ -10,17 +7,11 @@ const defaultLog = getLogger('queries/permit/permit-view-queries'); * @returns {SQLStatement} sql query object */ export const getNonSamplingPermitsSQL = (systemUserId: number | null): SQLStatement | null => { - defaultLog.debug({ - label: 'getNonSamplingPermitsSQL', - message: 'params', - systemUserId - }); - if (!systemUserId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT permit_id, number, @@ -32,15 +23,6 @@ export const getNonSamplingPermitsSQL = (systemUserId: number | null): SQLStatem AND project_id IS NULL; `; - - defaultLog.debug({ - label: 'getNonSamplingPermitsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -50,17 +32,11 @@ export const getNonSamplingPermitsSQL = (systemUserId: number | null): SQLStatem * @returns {SQLStatement} sql query object */ export const getAllPermitsSQL = (systemUserId: number | null): SQLStatement | null => { - defaultLog.debug({ - label: 'getAllPermitsSQL', - message: 'params', - systemUserId - }); - if (!systemUserId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT per.permit_id as id, per.number, @@ -79,13 +55,4 @@ export const getAllPermitsSQL = (systemUserId: number | null): SQLStatement | nu WHERE system_user_id = ${systemUserId}; `; - - defaultLog.debug({ - label: 'getAllPermitsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; diff --git a/api/src/queries/project-participation/index.ts b/api/src/queries/project-participation/index.ts new file mode 100644 index 0000000000..2d0379167f --- /dev/null +++ b/api/src/queries/project-participation/index.ts @@ -0,0 +1,3 @@ +import * as projectParticipation from './project-participation-queries'; + +export default { ...projectParticipation }; diff --git a/api/src/queries/project-participation/project-participation-queries.test.ts b/api/src/queries/project-participation/project-participation-queries.test.ts new file mode 100644 index 0000000000..ab9cdb82e3 --- /dev/null +++ b/api/src/queries/project-participation/project-participation-queries.test.ts @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import { describe } from 'mocha'; +import { + addProjectRoleByRoleNameSQL, + deleteProjectParticipationSQL, + getAllProjectParticipantsSQL, + getAllUserProjectsSQL, + getProjectParticipationBySystemUserSQL +} from './project-participation-queries'; + +describe('getAllUserProjectsSQL', () => { + it('returns null response when null userId provided', () => { + const response = getAllUserProjectsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when null valid params provided', () => { + const response = getAllUserProjectsSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getProjectParticipationBySystemUserSQL', () => { + it('returns null response when null projectId provided', () => { + const response = getProjectParticipationBySystemUserSQL((null as unknown) as number, 2); + + expect(response).to.be.null; + }); + + it('returns null response when null systemUserId provided', () => { + const response = getProjectParticipationBySystemUserSQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when null valid params provided', () => { + const response = getProjectParticipationBySystemUserSQL(1, 2); + + expect(response).to.not.be.null; + }); +}); + +describe('getAllProjectParticipantsSQL', () => { + it('returns null response when null projectId provided', () => { + const response = getAllProjectParticipantsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns null response when valid params provided', () => { + const response = getAllProjectParticipantsSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('addProjectRoleByRoleNameSQL', () => { + it('returns null response when null projectId provided', () => { + const response = addProjectRoleByRoleNameSQL((null as unknown) as number, 2, 'role'); + + expect(response).to.be.null; + }); + + it('returns null response when null systemUserId provided', () => { + const response = addProjectRoleByRoleNameSQL(1, (null as unknown) as number, 'role'); + + expect(response).to.be.null; + }); + + it('returns null response when null/empty projectParticipantRole provided', () => { + const response = addProjectRoleByRoleNameSQL(1, 2, ''); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = addProjectRoleByRoleNameSQL(1, 2, 'role'); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteProjectParticipationSQL', () => { + it('returns null response when null projectParticipationId provided', () => { + const response = deleteProjectParticipationSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deleteProjectParticipationSQL(1); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/project-participation/project-participation-queries.ts b/api/src/queries/project-participation/project-participation-queries.ts new file mode 100644 index 0000000000..aa4b9b3b20 --- /dev/null +++ b/api/src/queries/project-participation/project-participation-queries.ts @@ -0,0 +1,246 @@ +import SQL, { SQLStatement } from 'sql-template-strings'; + +/** + * SQL query to get all projects from user Id. + * + * @param {userId} userId + * @returns {SQLStatement} sql query object + */ +export const getParticipantsFromAllSystemUsersProjectsSQL = (systemUserId: number): SQLStatement | null => { + if (!systemUserId) { + return null; + } + + return SQL` + SELECT + pp.project_participation_id, + pp.project_id, + pp.system_user_id, + pp.project_role_id, + pr.name project_role_name + FROM + project_participation pp + LEFT JOIN + project p + ON + pp.project_id = p.project_id + LEFT JOIN + project_role pr + ON + pr.project_role_id = pp.project_role_id + WHERE + pp.project_id in ( + SELECT + p.project_id + FROM + project_participation pp + LEFT JOIN + project p + ON + pp.project_id = p.project_id + WHERE + pp.system_user_id = ${systemUserId} + ); + `; +}; + +/** + * SQL query to get all projects from user Id. + * + * @param {userId} userId + * @returns {SQLStatement} sql query object + */ +export const getAllUserProjectsSQL = (userId: number): SQLStatement | null => { + if (!userId) { + return null; + } + + return SQL` + SELECT + p.project_id, + p.name, + pp.system_user_id, + pp.project_role_id, + pp.project_participation_id + FROM + project_participation pp + LEFT JOIN + project p + ON + pp.project_id = p.project_id + WHERE + pp.system_user_id = ${userId}; + `; +}; + +/** + * SQL query to add a single project role to a user. + * + * @param {number} projectId + * @param {number} systemUserId + * @param {string} projectParticipantRole + * @return {*} {(SQLStatement | null)} + */ +export const getProjectParticipationBySystemUserSQL = ( + projectId: number, + systemUserId: number +): SQLStatement | null => { + if (!projectId || !systemUserId) { + return null; + } + + return SQL` + SELECT + pp.project_id, + pp.system_user_id, + su.record_end_date, + array_remove(array_agg(pr.project_role_id), NULL) AS project_role_ids, + array_remove(array_agg(pr.name), NULL) AS project_role_names + FROM + project_participation pp + LEFT JOIN + project_role pr + ON + pp.project_role_id = pr.project_role_id + LEFT JOIN + system_user su + ON + pp.system_user_id = su.system_user_id + WHERE + pp.project_id = ${projectId} + AND + pp.system_user_id = ${systemUserId} + AND + su.record_end_date is NULL + GROUP BY + pp.project_id, + pp.system_user_id, + su.record_end_date ; + `; +}; + +/** + * SQL query to get all project participants. + * + * @param {projectId} projectId + * @returns {SQLStatement} sql query object + */ +export const getAllProjectParticipantsSQL = (projectId: number): SQLStatement | null => { + if (!projectId) { + return null; + } + + return SQL` + SELECT + pp.project_participation_id, + pp.project_id, + pp.system_user_id, + pp.project_role_id, + pr.name project_role_name, + su.user_identifier, + su.user_identity_source_id + FROM + project_participation pp + LEFT JOIN + system_user su + ON + pp.system_user_id = su.system_user_id + LEFT JOIN + project_role pr + ON + pr.project_role_id = pp.project_role_id + WHERE + pp.project_id = ${projectId}; + `; +}; + +/** + * SQL query to add a single project role to a user. + * + * @param {number} projectId + * @param {number} systemUserId + * @param {string} projectParticipantRole + * @return {*} {(SQLStatement | null)} + */ +export const addProjectRoleByRoleNameSQL = ( + projectId: number, + systemUserId: number, + projectParticipantRole: string +): SQLStatement | null => { + if (!projectId || !systemUserId || !projectParticipantRole) { + return null; + } + + return SQL` + INSERT INTO project_participation ( + project_id, + system_user_id, + project_role_id + ) + ( + SELECT + ${projectId}, + ${systemUserId}, + project_role_id + FROM + project_role + WHERE + name = ${projectParticipantRole} + ) + RETURNING + *; + `; +}; + +/** + * SQL query to add a single project role to a user. + * + * @param {number} projectId + * @param {number} systemUserId + * @param {string} projectParticipantRole + * @return {*} {(SQLStatement | null)} + */ +export const addProjectRoleByRoleIdSQL = ( + projectId: number, + systemUserId: number, + projectParticipantRoleId: number +): SQLStatement | null => { + if (!projectId || !systemUserId || !projectParticipantRoleId) { + return null; + } + + return SQL` + INSERT INTO project_participation ( + project_id, + system_user_id, + project_role_id + ) VALUES ( + ${projectId}, + ${systemUserId}, + ${projectParticipantRoleId} + ) + RETURNING + *; + `; +}; + +/** + * SQL query to delete a single project participation record. + * + * @param {number} projectParticipationId + * @return {*} {(SQLStatement | null)} + */ +export const deleteProjectParticipationSQL = (projectParticipationId: number): SQLStatement | null => { + if (!projectParticipationId) { + return null; + } + + return SQL` + DELETE FROM + project_participation + WHERE + project_participation_id = ${projectParticipationId} + RETURNING + *; + `; +}; diff --git a/api/src/queries/draft-queries.test.ts b/api/src/queries/project/draft/draft-queries.test.ts similarity index 100% rename from api/src/queries/draft-queries.test.ts rename to api/src/queries/project/draft/draft-queries.test.ts diff --git a/api/src/queries/draft-queries.ts b/api/src/queries/project/draft/draft-queries.ts similarity index 68% rename from api/src/queries/draft-queries.ts rename to api/src/queries/project/draft/draft-queries.ts index cc87850c7c..158f35c82c 100644 --- a/api/src/queries/draft-queries.ts +++ b/api/src/queries/project/draft/draft-queries.ts @@ -1,7 +1,4 @@ import SQL, { SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../utils/logger'; - -const defaultLog = getLogger('queries/draft-queries'); /** * SQL query to insert a row in the webform_draft table. @@ -12,8 +9,6 @@ const defaultLog = getLogger('queries/draft-queries'); * @return {*} {(SQLStatement | null)} */ export const postDraftSQL = (systemUserId: number, name: string, data: unknown): SQLStatement | null => { - defaultLog.debug({ label: 'postDraftSQL', message: 'params', name, data }); - if (!systemUserId || !name || !data) { return null; } @@ -34,13 +29,6 @@ export const postDraftSQL = (systemUserId: number, name: string, data: unknown): update_date::timestamptz; `; - defaultLog.debug({ - label: 'postDraftSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -53,8 +41,6 @@ export const postDraftSQL = (systemUserId: number, name: string, data: unknown): * @return {*} {(SQLStatement | null)} */ export const putDraftSQL = (id: number, name: string, data: unknown): SQLStatement | null => { - defaultLog.debug({ label: 'putDraftSQL', message: 'params', name, data }); - if (!id || !name || !data) { return null; } @@ -73,13 +59,6 @@ export const putDraftSQL = (id: number, name: string, data: unknown): SQLStateme update_date::timestamptz; `; - defaultLog.debug({ - label: 'putDraftSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -90,8 +69,6 @@ export const putDraftSQL = (id: number, name: string, data: unknown): SQLStateme * @return {SQLStatement} {(SQLStatement | null)} */ export const getDraftsSQL = (systemUserId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getDraftsSQL', message: 'params', systemUserId }); - if (!systemUserId) { return null; } @@ -106,13 +83,6 @@ export const getDraftsSQL = (systemUserId: number): SQLStatement | null => { system_user_id = ${systemUserId}; `; - defaultLog.debug({ - label: 'getDraftsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -123,8 +93,6 @@ export const getDraftsSQL = (systemUserId: number): SQLStatement | null => { * @return {SQLStatement} {(SQLStatement | null)} */ export const getDraftSQL = (draftId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getDraftSQL', message: 'params', draftId }); - if (!draftId) { return null; } @@ -140,13 +108,6 @@ export const getDraftSQL = (draftId: number): SQLStatement | null => { webform_draft_id = ${draftId}; `; - defaultLog.debug({ - label: 'getDraftSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -157,8 +118,6 @@ export const getDraftSQL = (draftId: number): SQLStatement | null => { * @return {SQLStatement} {(SQLStatement) | null} */ export const deleteDraftSQL = (draftId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteDraftSQL', message: 'params', draftId }); - if (!draftId) { return null; } @@ -168,12 +127,5 @@ export const deleteDraftSQL = (draftId: number): SQLStatement | null => { WHERE webform_draft_id = ${draftId}; `; - defaultLog.debug({ - label: 'deleteDraftSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/project/draft/index.ts b/api/src/queries/project/draft/index.ts new file mode 100644 index 0000000000..aa0bc59600 --- /dev/null +++ b/api/src/queries/project/draft/index.ts @@ -0,0 +1,3 @@ +import * as draft from './draft-queries'; + +export default { ...draft }; diff --git a/api/src/queries/project/index.ts b/api/src/queries/project/index.ts new file mode 100644 index 0000000000..f34215dc0d --- /dev/null +++ b/api/src/queries/project/index.ts @@ -0,0 +1,15 @@ +import draft from './draft'; +import * as projectAttachments from './project-attachments-queries'; +import * as projectCreate from './project-create-queries'; +import * as projectDelete from './project-delete-queries'; +import * as projectUpdate from './project-update-queries'; +import * as projectView from './project-view-queries'; + +export default { + ...projectAttachments, + ...projectCreate, + ...projectDelete, + ...projectUpdate, + ...projectView, + draft +}; diff --git a/api/src/queries/project/project-attachments-queries.test.ts b/api/src/queries/project/project-attachments-queries.test.ts index 3edaf8f059..84dfa76edd 100644 --- a/api/src/queries/project/project-attachments-queries.test.ts +++ b/api/src/queries/project/project-attachments-queries.test.ts @@ -1,18 +1,51 @@ import { expect } from 'chai'; import { describe } from 'mocha'; +import { IReportAttachmentAuthor, PutReportAttachmentMetadata } from '../../models/project-survey-attachments'; import { - getProjectAttachmentsSQL, deleteProjectAttachmentSQL, + deleteProjectReportAttachmentAuthorsSQL, + deleteProjectReportAttachmentSQL, + getProjectAttachmentByFileNameSQL, getProjectAttachmentS3KeySQL, + getProjectAttachmentsSQL, + getProjectReportAttachmentByFileNameSQL, + getProjectReportAttachmentS3KeySQL, + getProjectReportAttachmentSQL, + getProjectReportAttachmentsSQL, + getProjectReportAuthorsSQL, + insertProjectReportAttachmentAuthorSQL, postProjectAttachmentSQL, - getProjectAttachmentByFileNameSQL, - putProjectAttachmentSQL, postProjectReportAttachmentSQL, - getProjectReportAttachmentsSQL, + putProjectAttachmentSQL, putProjectReportAttachmentSQL, - deleteProjectReportAttachmentSQL + updateProjectReportAttachmentMetadataSQL } from './project-attachments-queries'; +const post_sample_attachment_meta = { + title: 'title', + year_published: 2000, + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ], + description: 'description' +}; + +const put_sample_attachment_meta = { + title: 'title', + year_published: 2000, + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ], + description: 'description', + revision_count: 0 +}; + describe('getProjectAttachmentsSQL', () => { it('returns null response when null projectId provided', () => { const response = getProjectAttachmentsSQL((null as unknown) as number); @@ -89,6 +122,26 @@ describe('getProjectAttachmentS3KeySQL', () => { }); }); +describe('getProjectReportAttachmentS3KeySQL', () => { + it('returns null response when null projectId provided', () => { + const response = getProjectReportAttachmentS3KeySQL((null as unknown) as number, 1); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmentId provided', () => { + const response = getProjectReportAttachmentS3KeySQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId and attachmentId provided', () => { + const response = getProjectReportAttachmentS3KeySQL(1, 2); + + expect(response).to.not.be.null; + }); +}); + describe('postProjectAttachmentSQL', () => { it('returns null response when null projectId provided', () => { const response = postProjectAttachmentSQL('name', 20, 'type', (null as unknown) as number, 'key'); @@ -129,31 +182,55 @@ describe('postProjectAttachmentSQL', () => { describe('postProjectReportAttachmentSQL', () => { it('returns null response when null projectId provided', () => { - const response = postProjectReportAttachmentSQL('name', 20, (null as unknown) as number, 'key'); + const response = postProjectReportAttachmentSQL( + 'name', + 20, + (null as unknown) as number, + 'key', + post_sample_attachment_meta + ); expect(response).to.be.null; }); it('returns null response when null fileName provided', () => { - const response = postProjectReportAttachmentSQL((null as unknown) as string, 20, 1, 'key'); + const response = postProjectReportAttachmentSQL( + (null as unknown) as string, + 20, + 1, + 'key', + post_sample_attachment_meta + ); expect(response).to.be.null; }); it('returns null response when null fileSize provided', () => { - const response = postProjectReportAttachmentSQL('name', (null as unknown) as number, 1, 'key'); + const response = postProjectReportAttachmentSQL( + 'name', + (null as unknown) as number, + 1, + 'key', + post_sample_attachment_meta + ); expect(response).to.be.null; }); it('returns null response when null key provided', () => { - const response = postProjectReportAttachmentSQL('name', 2, 1, (null as unknown) as string); + const response = postProjectReportAttachmentSQL( + 'name', + 2, + 1, + (null as unknown) as string, + post_sample_attachment_meta + ); expect(response).to.be.null; }); it('returns non null response when valid projectId and fileName and fileSize and key provided', () => { - const response = postProjectReportAttachmentSQL('name', 20, 1, 'key'); + const response = postProjectReportAttachmentSQL('name', 20, 1, 'key', post_sample_attachment_meta); expect(response).to.not.be.null; }); @@ -178,6 +255,25 @@ describe('getProjectAttachmentByFileNameSQL', () => { expect(response).to.not.be.null; }); }); +describe('getProjectReportAttachmentByFileNameSQL', () => { + it('returns null response when null projectId provided', () => { + const response = getProjectReportAttachmentByFileNameSQL((null as unknown) as number, 'name'); + + expect(response).to.be.null; + }); + + it('returns null response when null fileName provided', () => { + const response = getProjectReportAttachmentByFileNameSQL(1, (null as unknown) as string); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId and fileName provided', () => { + const response = getProjectReportAttachmentByFileNameSQL(1, 'name'); + + expect(response).to.not.be.null; + }); +}); describe('putProjectAttachmentSQL', () => { it('returns null response when null projectId provided', () => { @@ -207,19 +303,133 @@ describe('putProjectAttachmentSQL', () => { describe('putProjectReportAttachmentSQL', () => { it('returns null response when null projectId provided', () => { - const response = putProjectReportAttachmentSQL((null as unknown) as number, 'name'); + const response = putProjectReportAttachmentSQL((null as unknown) as number, 'name', put_sample_attachment_meta); expect(response).to.be.null; }); it('returns null response when null fileName provided', () => { - const response = putProjectReportAttachmentSQL(1, (null as unknown) as string); + const response = putProjectReportAttachmentSQL(1, (null as unknown) as string, put_sample_attachment_meta); expect(response).to.be.null; }); it('returns non null response when valid projectId and fileName provided', () => { - const response = putProjectReportAttachmentSQL(1, 'name'); + const response = putProjectReportAttachmentSQL(1, 'name', put_sample_attachment_meta); + + expect(response).to.not.be.null; + }); +}); + +describe('updateProjectReportAttachmentMetadataSQL', () => { + it('returns null response when null projectId provided', () => { + const response = updateProjectReportAttachmentMetadataSQL( + (null as unknown) as number, + 1, + put_sample_attachment_meta + ); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmentId provided', () => { + const response = updateProjectReportAttachmentMetadataSQL( + 1, + (null as unknown) as number, + put_sample_attachment_meta + ); + + expect(response).to.be.null; + }); + + it('returns null response when null metadata provided', () => { + const response = updateProjectReportAttachmentMetadataSQL(1, 1, (null as unknown) as PutReportAttachmentMetadata); + + expect(response).to.be.null; + }); + + it('returns not null response when valid parameters are provided', () => { + const response = updateProjectReportAttachmentMetadataSQL(1, 1, put_sample_attachment_meta); + + expect(response).to.not.be.null; + }); +}); + +describe('insertProjectReportAttachmentAuthorSQL', () => { + const report_attachment_author: IReportAttachmentAuthor = { + first_name: 'John', + last_name: 'Smith' + }; + it('returns null response when null attachmentId provided', () => { + const response = insertProjectReportAttachmentAuthorSQL((null as unknown) as number, report_attachment_author); + + expect(response).to.be.null; + }); + + it('returns null response when null report author provided', () => { + const response = insertProjectReportAttachmentAuthorSQL(1, (null as unknown) as IReportAttachmentAuthor); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmmentId and null report author are provided', () => { + const response = insertProjectReportAttachmentAuthorSQL( + (null as unknown) as number, + (null as unknown) as IReportAttachmentAuthor + ); + expect(response).to.be.null; + }); + + it('returns not null response when valid parameters are provided', () => { + const response = insertProjectReportAttachmentAuthorSQL(1, report_attachment_author); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteProjectReportAttachmentAuthorsSQL', () => { + it('returns null response when null attachmentId provided', () => { + const response = deleteProjectReportAttachmentAuthorsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns not null response when valid params are provided', () => { + const response = deleteProjectReportAttachmentAuthorsSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getProjectReportAttachmentSQL', () => { + it('returns null response when null projectId provided', () => { + const response = getProjectReportAttachmentSQL((null as unknown) as number, 1); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmentId provided', () => { + const response = getProjectReportAttachmentSQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectId and attachmentId provided', () => { + const response = getProjectReportAttachmentSQL(1, 2); + + expect(response).to.not.be.null; + }); +}); + +describe('getProjectReportAuthorSQL', () => { + it('returns null response when null projectReportAttachmentId provided', () => { + const response = getProjectReportAuthorsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectReportAttachmentId provided', () => { + const response = getProjectReportAuthorsSQL(1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/project/project-attachments-queries.ts b/api/src/queries/project/project-attachments-queries.ts index dbc70b93e1..8392690d76 100644 --- a/api/src/queries/project/project-attachments-queries.ts +++ b/api/src/queries/project/project-attachments-queries.ts @@ -1,7 +1,9 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/project/project-attachments-queries'); +import { + IReportAttachmentAuthor, + PostReportAttachmentMetadata, + PutReportAttachmentMetadata +} from '../../models/project-survey-attachments'; /** * SQL query to get attachments for a single project. @@ -10,8 +12,6 @@ const defaultLog = getLogger('queries/project/project-attachments-queries'); * @returns {SQLStatement} sql query object */ export const getProjectAttachmentsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectAttachmentsSQL', message: 'params', projectId }); - if (!projectId) { return null; } @@ -32,13 +32,6 @@ export const getProjectAttachmentsSQL = (projectId: number): SQLStatement | null project_id = ${projectId}; `; - defaultLog.debug({ - label: 'getProjectAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -49,8 +42,6 @@ export const getProjectAttachmentsSQL = (projectId: number): SQLStatement | null * @returns {SQLStatement} sql query object */ export const getProjectReportAttachmentsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectReportAttachmentsSQL', message: 'params', projectId }); - if (!projectId) { return null; } @@ -70,13 +61,6 @@ export const getProjectReportAttachmentsSQL = (projectId: number): SQLStatement project_id = ${projectId}; `; - defaultLog.debug({ - label: 'getProjectReportAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -87,8 +71,6 @@ export const getProjectReportAttachmentsSQL = (projectId: number): SQLStatement * @returns {SQLStatement} sql query object */ export const deleteProjectAttachmentSQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteProjectAttachmentSQL', message: 'params', attachmentId }); - if (!attachmentId) { return null; } @@ -102,13 +84,6 @@ export const deleteProjectAttachmentSQL = (attachmentId: number): SQLStatement | key; `; - defaultLog.debug({ - label: 'deleteProjectAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -119,8 +94,6 @@ export const deleteProjectAttachmentSQL = (attachmentId: number): SQLStatement | * @returns {SQLStatement} sql query object */ export const deleteProjectReportAttachmentSQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteProjectReportAttachmentSQL', message: 'params', attachmentId }); - if (!attachmentId) { return null; } @@ -134,13 +107,6 @@ export const deleteProjectReportAttachmentSQL = (attachmentId: number): SQLState key; `; - defaultLog.debug({ - label: 'deleteProjectReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -152,8 +118,6 @@ export const deleteProjectReportAttachmentSQL = (attachmentId: number): SQLState * @returns {SQLStatement} sql query object */ export const getProjectAttachmentS3KeySQL = (projectId: number, attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectAttachmentS3KeySQL', message: 'params', projectId }); - if (!projectId || !attachmentId) { return null; } @@ -169,12 +133,31 @@ export const getProjectAttachmentS3KeySQL = (projectId: number, attachmentId: nu project_attachment_id = ${attachmentId}; `; - defaultLog.debug({ - label: 'getProjectAttachmentS3KeySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to get S3 key of a report attachment for a single project. + * + * @param {number} projectId + * @param {number} attachmentId + * @returns {SQLStatement} sql query object + */ +export const getProjectReportAttachmentS3KeySQL = (projectId: number, attachmentId: number): SQLStatement | null => { + if (!projectId || !attachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + key + FROM + project_report_attachment + WHERE + project_id = ${projectId} + AND + project_report_attachment_id = ${attachmentId}; + `; return sqlStatement; }; @@ -196,16 +179,6 @@ export const postProjectAttachmentSQL = ( projectId: number, key: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectAttachmentSQL', - message: 'params', - fileName, - fileSize, - fileType, - projectId, - key - }); - if (!fileName || !fileSize || !fileType || !projectId || !key) { return null; } @@ -225,16 +198,10 @@ export const postProjectAttachmentSQL = ( ${key} ) RETURNING - project_attachment_id as id; + project_attachment_id as id, + revision_count; `; - defaultLog.debug({ - label: 'postProjectAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -251,55 +218,44 @@ export const postProjectReportAttachmentSQL = ( fileName: string, fileSize: number, projectId: number, - key: string + key: string, + attachmentMeta: PostReportAttachmentMetadata ): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectReportAttachmentSQL', - message: 'params', - fileName, - fileSize, - projectId, - key - }); - - if (!fileName || !fileSize || !projectId || !key) { + if ( + !fileName || + !fileSize || + !projectId || + !key || + !attachmentMeta?.title || + !attachmentMeta?.year_published || + !attachmentMeta?.description + ) { return null; } - // TODO: Replace hard-coded title, year and description - const title = 'Test Report'; - const year = '2021'; - const description = 'Test description'; - const sqlStatement: SQLStatement = SQL` INSERT INTO project_report_attachment ( project_id, + file_name, title, year, description, - file_name, file_size, key ) VALUES ( ${projectId}, - ${title}, - ${year}, - ${description}, ${fileName}, + ${attachmentMeta.title}, + ${attachmentMeta.year_published}, + ${attachmentMeta.description}, ${fileSize}, ${key} ) RETURNING - project_report_attachment_id as id; + project_report_attachment_id as id, + revision_count; `; - defaultLog.debug({ - label: 'postProjectReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -311,8 +267,6 @@ export const postProjectReportAttachmentSQL = ( * @returns {SQLStatement} sql query object */ export const getProjectAttachmentByFileNameSQL = (projectId: number, fileName: string): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectAttachmentByFileNameSQL', message: 'params', projectId }); - if (!projectId || !fileName) { return null; } @@ -332,12 +286,35 @@ export const getProjectAttachmentByFileNameSQL = (projectId: number, fileName: s file_name = ${fileName}; `; - defaultLog.debug({ - label: 'getProjectAttachmentByFileNameSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to get an attachment for a single project by project id and filename. + * + * @param {number} projectId + * @param {string} fileName + * @returns {SQLStatement} sql query object + */ +export const getProjectReportAttachmentByFileNameSQL = (projectId: number, fileName: string): SQLStatement | null => { + if (!projectId || !fileName) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + project_report_attachment_id as id, + file_name, + update_date, + create_date, + file_size + from + project_report_attachment + where + project_id = ${projectId} + and + file_name = ${fileName}; + `; return sqlStatement; }; @@ -351,8 +328,6 @@ export const getProjectAttachmentByFileNameSQL = (projectId: number, fileName: s * @returns {SQLStatement} sql query object */ export const putProjectAttachmentSQL = (projectId: number, fileName: string, fileType: string): SQLStatement | null => { - defaultLog.debug({ label: 'putProjectAttachmentSQL', message: 'params', projectId, fileName, fileType }); - if (!projectId || !fileName || !fileType) { return null; } @@ -366,16 +341,12 @@ export const putProjectAttachmentSQL = (projectId: number, fileName: string, fil WHERE file_name = ${fileName} AND - project_id = ${projectId}; + project_id = ${projectId} + RETURNING + project_attachment_id as id, + revision_count; `; - defaultLog.debug({ - label: 'putProjectAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -386,10 +357,18 @@ export const putProjectAttachmentSQL = (projectId: number, fileName: string, fil * @param {string} fileName * @returns {SQLStatement} sql query object */ -export const putProjectReportAttachmentSQL = (projectId: number, fileName: string): SQLStatement | null => { - defaultLog.debug({ label: 'putProjectReportAttachmentSQL', message: 'params', projectId, fileName }); - - if (!projectId || !fileName) { +export const putProjectReportAttachmentSQL = ( + projectId: number, + fileName: string, + attachmentMeta: PutReportAttachmentMetadata +): SQLStatement | null => { + if ( + !projectId || + !fileName || + !attachmentMeta?.title || + !attachmentMeta?.year_published || + !attachmentMeta?.description + ) { return null; } @@ -397,19 +376,172 @@ export const putProjectReportAttachmentSQL = (projectId: number, fileName: strin UPDATE project_report_attachment SET - file_name = ${fileName} + file_name = ${fileName}, + title = ${attachmentMeta.title}, + year = ${attachmentMeta.year_published}, + description = ${attachmentMeta.description} WHERE file_name = ${fileName} AND - project_id = ${projectId}; + project_id = ${projectId} + RETURNING + project_report_attachment_id as id, + revision_count; + `; + + return sqlStatement; +}; + +export interface ReportAttachmentMeta { + title: string; + description: string; + yearPublished: string; +} + +/** + * Update the metadata fields of project report attachment, for tjhe specified `projectId` and `attachmentId`. + * + * @param {number} projectId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const updateProjectReportAttachmentMetadataSQL = ( + projectId: number, + attachmentId: number, + metadata: PutReportAttachmentMetadata +): SQLStatement | null => { + if (!projectId || !attachmentId || !metadata) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + UPDATE + project_report_attachment + SET + title = ${metadata.title}, + year = ${metadata.year_published}, + description = ${metadata.description} + WHERE + project_id = ${projectId} + AND + project_report_attachment_id = ${attachmentId} + AND + revision_count = ${metadata.revision_count}; + `; + + return sqlStatement; +}; + +/** + * Insert a new project report attachment author record, for the specified `attachmentId` + * + * @param {number} attachmentId + * @param {IReportAttachmentAuthor} author + * @return {*} {(SQLStatement | null)} + */ +export const insertProjectReportAttachmentAuthorSQL = ( + attachmentId: number, + author: IReportAttachmentAuthor +): SQLStatement | null => { + if (!attachmentId || !author) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + INSERT INTO project_report_author ( + project_report_attachment_id, + first_name, + last_name + ) VALUES ( + ${attachmentId}, + ${author.first_name}, + ${author.last_name} + ); + `; + + return sqlStatement; +}; + +/** + * Delete all project report attachment author records, for the specified `attachmentId`. + * + * @param {number} attachmentId + * @return {*} {(SQLStatement | null)} + */ +export const deleteProjectReportAttachmentAuthorsSQL = (attachmentId: number): SQLStatement | null => { + if (!attachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + DELETE + FROM project_report_author + WHERE + project_report_attachment_id = ${attachmentId}; `; - defaultLog.debug({ - label: 'putProjectReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * Get the metadata fields of project report attachment, for the specified `projectId` and `attachmentId`. + * + * @param {number} projectId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const getProjectReportAttachmentSQL = (projectId: number, attachmentId: number): SQLStatement | null => { + if (!projectId || !attachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + project_report_attachment_id as attachment_id, + file_name, + title, + description, + year as year_published, + update_date, + create_date, + file_size, + key, + security_token, + revision_count + FROM + project_report_attachment + where + project_report_attachment_id = ${attachmentId} + and + project_id = ${projectId} + `; + + return sqlStatement; +}; + +/** + * Get the metadata fields of project report attachment, for the specified `projectId` and `attachmentId`. + * + * @param {number} projectId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const getProjectReportAuthorsSQL = (projectReportAttachmentId: number): SQLStatement | null => { + if (!projectReportAttachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + project_report_author.* + FROM + project_report_author + where + project_report_attachment_id = ${projectReportAttachmentId} + `; return sqlStatement; }; diff --git a/api/src/queries/project/project-create-queries.ts b/api/src/queries/project/project-create-queries.ts index 3a7a61095a..de27d71546 100644 --- a/api/src/queries/project/project-create-queries.ts +++ b/api/src/queries/project/project-create-queries.ts @@ -3,14 +3,10 @@ import { PostCoordinatorData, PostFundingSource, PostLocationData, - PostProjectData, PostObjectivesData, - PostProjectObject + PostProjectData } from '../../models/project-create'; -import { getLogger } from '../../utils/logger'; -import { generateGeometryCollectionSQL } from '../generate-geometry-collection'; - -const defaultLog = getLogger('queries/project/project-create-queries'); +import { queries } from '../queries'; /** * SQL query to insert a project row. @@ -21,8 +17,6 @@ const defaultLog = getLogger('queries/project/project-create-queries'); export const postProjectSQL = ( project: PostProjectData & PostLocationData & PostCoordinatorData & PostObjectivesData ): SQLStatement | null => { - defaultLog.debug({ label: 'postProjectSQL', message: 'params', PostProjectObject }); - if (!project) { return null; } @@ -62,7 +56,7 @@ export const postProjectSQL = ( `; if (project.geometry && project.geometry.length) { - const geometryCollectionSQL = generateGeometryCollectionSQL(project.geometry); + const geometryCollectionSQL = queries.spatial.generateGeometryCollectionSQL(project.geometry); sqlStatement.append(SQL` ,public.geography( @@ -87,13 +81,6 @@ export const postProjectSQL = ( project_id as id; `); - defaultLog.debug({ - label: 'postProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -107,8 +94,6 @@ export const postProjectFundingSourceSQL = ( fundingSource: PostFundingSource, projectId: number ): SQLStatement | null => { - defaultLog.debug({ label: 'postProjectFundingSourceSQL', message: 'params', fundingSource, projectId }); - if (!fundingSource || !projectId) { return null; } @@ -133,13 +118,6 @@ export const postProjectFundingSourceSQL = ( project_funding_source_id as id; `; - defaultLog.debug({ - label: 'postProjectFundingSourceSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -153,13 +131,6 @@ export const postProjectStakeholderPartnershipSQL = ( stakeholderPartnership: string, projectId: number ): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectStakeholderPartnershipSQL', - message: 'params', - stakeholderPartnership, - projectId - }); - if (!stakeholderPartnership || !projectId) { return null; } @@ -177,13 +148,6 @@ export const postProjectStakeholderPartnershipSQL = ( stakeholder_partnership_id as id; `; - defaultLog.debug({ - label: 'postPermitNumberWithSamplingSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -194,13 +158,6 @@ export const postProjectStakeholderPartnershipSQL = ( * @returns {SQLStatement} sql query object */ export const postProjectIndigenousNationSQL = (indigenousNationId: number, projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectIndigenousNationSQL', - message: 'params', - indigenousNationId, - projectId - }); - if (!indigenousNationId || !projectId) { return null; } @@ -215,16 +172,9 @@ export const postProjectIndigenousNationSQL = (indigenousNationId: number, proje ${indigenousNationId} ) RETURNING - project_first_nation_id as id; + first_nations_id as id; `; - defaultLog.debug({ - label: 'postProjectIndigenousNationSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -236,13 +186,6 @@ export const postProjectIndigenousNationSQL = (indigenousNationId: number, proje * @returns {SQLStatement} sql query object */ export const postProjectIUCNSQL = (iucn3_id: number, project_id: number): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectIUCNSQL', - message: 'params', - iucn3_id, - project_id - }); - if (!iucn3_id || !project_id) { return null; } @@ -259,13 +202,6 @@ export const postProjectIUCNSQL = (iucn3_id: number, project_id: number): SQLSta project_iucn_action_classification_id as id; `; - defaultLog.debug({ - label: 'postProjectIUCNSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -277,13 +213,6 @@ export const postProjectIUCNSQL = (iucn3_id: number, project_id: number): SQLSta * @returns {SQLStatement} sql query object */ export const postProjectActivitySQL = (activityId: number, projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'postProjectActivity', - message: 'params', - activityId, - projectId - }); - if (!activityId || !projectId) { return null; } @@ -300,12 +229,5 @@ export const postProjectActivitySQL = (activityId: number, projectId: number): S project_activity_id as id; `; - defaultLog.debug({ - label: 'postProjectActivity', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/project/project-delete-queries.test.ts b/api/src/queries/project/project-delete-queries.test.ts index 8397ed593b..72a8c590ed 100644 --- a/api/src/queries/project/project-delete-queries.test.ts +++ b/api/src/queries/project/project-delete-queries.test.ts @@ -4,10 +4,10 @@ import { deleteActivitiesSQL, deleteIndigenousPartnershipsSQL, deleteIUCNSQL, - deleteStakeholderPartnershipsSQL, - deleteProjectFundingSourceSQL, deletePermitSQL, - deleteProjectSQL + deleteProjectFundingSourceSQL, + deleteProjectSQL, + deleteStakeholderPartnershipsSQL } from './project-delete-queries'; describe('deleteIUCNSQL', () => { diff --git a/api/src/queries/project/project-delete-queries.ts b/api/src/queries/project/project-delete-queries.ts index 4e7610f732..2cbffdc999 100644 --- a/api/src/queries/project/project-delete-queries.ts +++ b/api/src/queries/project/project-delete-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/project/project-delete-queries'); /** * SQL query to delete project indigenous partnership rows (project_first_nations) @@ -10,12 +7,6 @@ const defaultLog = getLogger('queries/project/project-delete-queries'); * @returns {SQLStatement} sql query object */ export const deleteIndigenousPartnershipsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteIndigenousPartnershipsSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } @@ -27,13 +18,6 @@ export const deleteIndigenousPartnershipsSQL = (projectId: number): SQLStatement project_id = ${projectId}; `; - defaultLog.debug({ - label: 'deleteIndigenousPartnershipsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -44,12 +28,6 @@ export const deleteIndigenousPartnershipsSQL = (projectId: number): SQLStatement * @returns {SQLStatement} sql query object */ export const deletePermitSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deletePermitSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } @@ -61,13 +39,6 @@ export const deletePermitSQL = (projectId: number): SQLStatement | null => { project_id = ${projectId}; `; - defaultLog.debug({ - label: 'deletePermitSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -78,12 +49,6 @@ export const deletePermitSQL = (projectId: number): SQLStatement | null => { * @returns {SQLStatement} sql query object */ export const deleteStakeholderPartnershipsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteStakeholderPartnershipsSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } @@ -95,13 +60,6 @@ export const deleteStakeholderPartnershipsSQL = (projectId: number): SQLStatemen project_id = ${projectId}; `; - defaultLog.debug({ - label: 'deleteStakeholderPartnershipsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -112,12 +70,6 @@ export const deleteStakeholderPartnershipsSQL = (projectId: number): SQLStatemen * @returns {SQLStatement} sql query object */ export const deleteIUCNSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteIUCNSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } @@ -129,13 +81,6 @@ export const deleteIUCNSQL = (projectId: number): SQLStatement | null => { project_id = ${projectId}; `; - defaultLog.debug({ - label: 'deleteProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -146,12 +91,6 @@ export const deleteIUCNSQL = (projectId: number): SQLStatement | null => { * @returns {SQLStatement} sql query object */ export const deleteActivitiesSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteActivitiesSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } @@ -163,13 +102,6 @@ export const deleteActivitiesSQL = (projectId: number): SQLStatement | null => { project_id = ${projectId}; `; - defaultLog.debug({ - label: 'deleteActivitiesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -184,13 +116,6 @@ export const deleteProjectFundingSourceSQL = ( projectId: number | undefined, pfsId: number | undefined ): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteProjectFundingSourceSQL', - message: 'params', - projectId, - pfsId - }); - if (!projectId || !pfsId) { return null; } @@ -204,13 +129,6 @@ export const deleteProjectFundingSourceSQL = ( project_funding_source_id = ${pfsId}; `; - defaultLog.debug({ - label: 'deleteProjectFundingSourceSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -221,24 +139,11 @@ export const deleteProjectFundingSourceSQL = ( * @returns {SQLStatement} sql query object */ export const deleteProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteProjectSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } const sqlStatement: SQLStatement = SQL`call api_delete_project(${projectId})`; - defaultLog.debug({ - label: 'deleteProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/project/project-update-queries.test.ts b/api/src/queries/project/project-update-queries.test.ts index 37ab36c841..f7cf2df7df 100644 --- a/api/src/queries/project/project-update-queries.test.ts +++ b/api/src/queries/project/project-update-queries.test.ts @@ -2,20 +2,20 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PutCoordinatorData, + PutFundingSource, PutLocationData, PutObjectivesData, - PutProjectData, - PutFundingSource + PutProjectData } from '../../models/project-update'; import { - getIndigenousPartnershipsByProjectSQL, getCoordinatorByProjectSQL, + getIndigenousPartnershipsByProjectSQL, getIUCNActionClassificationByProjectSQL, getObjectivesByProjectSQL, + getPermitsByProjectSQL, getProjectByProjectSQL, - putProjectSQL, putProjectFundingSourceSQL, - getPermitsByProjectSQL, + putProjectSQL, updateProjectPublishStatusSQL } from './project-update-queries'; diff --git a/api/src/queries/project/project-update-queries.ts b/api/src/queries/project/project-update-queries.ts index 10828d46d7..913a219027 100644 --- a/api/src/queries/project/project-update-queries.ts +++ b/api/src/queries/project/project-update-queries.ts @@ -1,15 +1,12 @@ import { SQL, SQLStatement } from 'sql-template-strings'; import { PutCoordinatorData, + PutFundingSource, PutLocationData, PutObjectivesData, - PutProjectData, - PutFundingSource + PutProjectData } from '../../models/project-update'; -import { getLogger } from '../../utils/logger'; -import { generateGeometryCollectionSQL } from '../generate-geometry-collection'; - -const defaultLog = getLogger('queries/project/project-update-queries'); +import { queries } from '../queries'; /** * SQL query to get IUCN action classifications. @@ -18,13 +15,11 @@ const defaultLog = getLogger('queries/project/project-update-queries'); * @returns {SQLStatement} sql query object */ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIUCNActionClassificationByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT ical1c.iucn_conservation_action_level_1_classification_id as classification, ical2s.iucn_conservation_action_level_2_subclassification_id as subClassification1, @@ -50,15 +45,6 @@ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLS ical2s.iucn_conservation_action_level_2_subclassification_id, ical3s.iucn_conservation_action_level_3_subclassification_id; `; - - defaultLog.debug({ - label: 'getIUCNActionClassificationByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -67,13 +53,11 @@ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLS * @returns {SQLStatement} sql query object */ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIndigenousPartnershipsByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT project_first_nation_id as id FROM @@ -83,15 +67,6 @@ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLSta GROUP BY project_first_nation_id; `; - - defaultLog.debug({ - label: 'getIndigenousPartnershipsByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -100,13 +75,11 @@ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLSta * @returns {SQLStatement} sql query object */ export const getPermitsByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPermitsByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT number, type @@ -115,15 +88,6 @@ export const getPermitsByProjectSQL = (projectId: number): SQLStatement | null = WHERE project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getPermitsByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -133,12 +97,11 @@ export const getPermitsByProjectSQL = (projectId: number): SQLStatement | null = * @return {*} {(SQLStatement | null)} */ export const getCoordinatorByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getCoordinatorByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + + return SQL` SELECT coordinator_first_name, coordinator_last_name, @@ -151,15 +114,6 @@ export const getCoordinatorByProjectSQL = (projectId: number): SQLStatement | nu WHERE project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getCoordinatorByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -169,13 +123,11 @@ export const getCoordinatorByProjectSQL = (projectId: number): SQLStatement | nu * @return {*} {(SQLStatement | null)} */ export const getProjectByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT name, project_type_id as pt_id, @@ -187,15 +139,6 @@ export const getProjectByProjectSQL = (projectId: number): SQLStatement | null = WHERE project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getProjectByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -212,17 +155,6 @@ export const putProjectSQL = ( coordinator: PutCoordinatorData | null, revision_count: number ): SQLStatement | null => { - defaultLog.debug({ - label: 'putProjectSQL', - message: 'params', - projectId, - project, - location, - objectives, - coordinator, - revision_count - }); - if (!projectId) { return null; } @@ -250,7 +182,7 @@ export const putProjectSQL = ( const geometrySQLStatement = SQL`geography = `; if (location.geometry && location.geometry.length) { - const geometryCollectionSQL = generateGeometryCollectionSQL(location.geometry); + const geometryCollectionSQL = queries.spatial.generateGeometryCollectionSQL(location.geometry); geometrySQLStatement.append(SQL` public.geography( @@ -297,13 +229,6 @@ export const putProjectSQL = ( revision_count = ${revision_count}; `); - defaultLog.debug({ - label: 'putProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -314,13 +239,11 @@ export const putProjectSQL = ( * @return {*} {(SQLStatement | null)} */ export const getObjectivesByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getObjectivesByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT objectives, caveats, @@ -330,15 +253,6 @@ export const getObjectivesByProjectSQL = (projectId: number): SQLStatement | nul WHERE project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getObjectivesByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -351,13 +265,11 @@ export const putProjectFundingSourceSQL = ( fundingSource: PutFundingSource | null, projectId: number ): SQLStatement | null => { - defaultLog.debug({ label: 'putProjectFundingSourceSQL', message: 'params', fundingSource, projectId }); - if (!fundingSource || !projectId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO project_funding_source ( project_id, investment_action_category_id, @@ -376,15 +288,6 @@ export const putProjectFundingSourceSQL = ( RETURNING project_funding_source_id as id; `; - - defaultLog.debug({ - label: 'putProjectFundingSourceSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -395,8 +298,6 @@ export const putProjectFundingSourceSQL = ( * @returns {SQLStatement} sql query object */ export const updateProjectPublishStatusSQL = (projectId: number, publish: boolean): SQLStatement | null => { - defaultLog.debug({ label: 'updateProjectPublishStatusSQL', message: 'params', projectId, publish }); - if (!projectId) { return null; } @@ -414,12 +315,5 @@ export const updateProjectPublishStatusSQL = (projectId: number, publish: boolea } sqlStatement.append(SQL` RETURNING project_id as id;`); - defaultLog.debug({ - label: 'updateProjectPublishStatusSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/project/project-view-queries.test.ts b/api/src/queries/project/project-view-queries.test.ts index e540276e96..f81896f1f9 100644 --- a/api/src/queries/project/project-view-queries.test.ts +++ b/api/src/queries/project/project-view-queries.test.ts @@ -1,11 +1,15 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { + getActivitiesByProjectSQL, + getFundingSourceByProjectSQL, getIndigenousPartnershipsByProjectSQL, getIUCNActionClassificationByProjectSQL, + getLocationByProjectSQL, getProjectListSQL, getProjectPermitsSQL, - getProjectSQL + getProjectSQL, + getStakeholderPartnershipsByProjectSQL } from './project-view-queries'; describe('getProjectSQL', () => { @@ -46,7 +50,7 @@ describe('getProjectListSQL', () => { expect(response).to.not.be.null; }); - it('returns a SQLStatement when filter fields provided (only coordinator agency)', () => { + it('returns a SQLStatement when filter fields provided (only contact agency)', () => { const response = getProjectListSQL(true, 1, { coordinator_agency: 'agency' }); expect(response).to.not.be.null; @@ -141,16 +145,72 @@ describe('getIndigenousPartnershipsByProjectSQL', () => { }); }); +describe('getStakeholderPartnershipsByProjectSQL', () => { + it('Null projectId', () => { + const response = getStakeholderPartnershipsByProjectSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('valid projectId', () => { + const response = getStakeholderPartnershipsByProjectSQL(1); + + expect(response).to.not.be.null; + }); +}); + describe('getProjectPermitsSQL', () => { - it('returns null response when null projectId provided', () => { + it('Null projectId', () => { const response = getProjectPermitsSQL((null as unknown) as number); expect(response).to.be.null; }); - it('returns non null response when valid projectId provided', () => { + it('valid projectId', () => { const response = getProjectPermitsSQL(1); expect(response).to.not.be.null; }); }); + +describe('getLocationByProjectSQL', () => { + it('Null projectId', () => { + const response = getLocationByProjectSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('valid projectId', () => { + const response = getLocationByProjectSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getActivitiesByProjectSQL', () => { + it('Null projectId', () => { + const response = getActivitiesByProjectSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('valid projectId', () => { + const response = getActivitiesByProjectSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getFundingSourceByProjectSQL', () => { + it('Null projectId', () => { + const response = getFundingSourceByProjectSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('valid projectId', () => { + const response = getFundingSourceByProjectSQL(1); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/project/project-view-queries.ts b/api/src/queries/project/project-view-queries.ts index b267affc55..292e4c7eeb 100644 --- a/api/src/queries/project/project-view-queries.ts +++ b/api/src/queries/project/project-view-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/project/project-view-queries'); /** * SQL query to get a single project. @@ -10,15 +7,14 @@ const defaultLog = getLogger('queries/project/project-view-queries'); * @returns {SQLStatement} sql query object */ export const getProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT project.project_id as id, + project.project_type_id as pt_id, project_type.name as type, project.name, project.objectives, @@ -47,15 +43,6 @@ export const getProjectSQL = (projectId: number): SQLStatement | null => { where project.project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -71,8 +58,6 @@ export const getProjectListSQL = ( systemUserId: number | null, filterFields?: any ): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectListSQL', message: 'params', isUserAdmin, systemUserId, filterFields }); - if (!systemUserId) { return null; } @@ -83,7 +68,7 @@ export const getProjectListSQL = ( p.name, p.start_date, p.end_date, - p.coordinator_agency_name, + p.coordinator_agency_name as coordinator_agency, p.publish_timestamp, pt.name as project_type, string_agg(DISTINCT pp.number, ', ') as permits_list @@ -103,14 +88,20 @@ export const getProjectListSQL = ( on s.project_id = p.project_id left outer join study_species as sp on sp.survey_id = s.survey_id - left outer join wldtaxonomic_units as wu - on wu.wldtaxonomic_units_id = sp.wldtaxonomic_units_id + where 1 = 1 `; if (!isUserAdmin) { - sqlStatement.append(SQL` where p.create_user = ${systemUserId}`); - } else { - sqlStatement.append(SQL` where 1 = 1`); + sqlStatement.append(SQL` + AND p.project_id IN ( + SELECT + project_id + FROM + project_participation + where + system_user_id = ${systemUserId} + ) + `); } if (filterFields && Object.keys(filterFields).length !== 0 && filterFields.constructor === Object) { @@ -153,7 +144,7 @@ export const getProjectListSQL = ( } if (filterFields.species && filterFields.species.length) { - sqlStatement.append(SQL` AND wu.wldtaxonomic_units_id =${filterFields.species[0]}`); + sqlStatement.append(SQL` AND sp.wldtaxonomic_units_id =${filterFields.species[0]}`); } if (filterFields.keyword) { @@ -176,13 +167,6 @@ export const getProjectListSQL = ( pt.name; `); - defaultLog.debug({ - label: 'getProjectListSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -193,17 +177,15 @@ export const getProjectListSQL = ( * @returns {SQLStatement} sql query object */ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIUCNActionClassificationByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - ical1c.name as classification, - ical2s.name as subClassification1, - ical3s.name as subClassification2 + ical1c.iucn_conservation_action_level_1_classification_id as classification, + ical2s.iucn_conservation_action_level_2_subclassification_id as subClassification1, + ical3s.iucn_conservation_action_level_3_subclassification_id as subClassification2 FROM project_iucn_action_classification as piac LEFT OUTER JOIN @@ -221,36 +203,27 @@ export const getIUCNActionClassificationByProjectSQL = (projectId: number): SQLS WHERE piac.project_id = ${projectId} GROUP BY - ical2s.name, - ical1c.name, - ical3s.name; + ical1c.iucn_conservation_action_level_1_classification_id, + ical2s.iucn_conservation_action_level_2_subclassification_id, + ical3s.iucn_conservation_action_level_3_subclassification_id; `; - - defaultLog.debug({ - label: 'getIUCNActionClassificationByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** * SQL query to get project indigenous partnerships. + * * @param {number} projectId * @returns {SQLStatement} sql query object */ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIndigenousPartnershipsByProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - fn.name as fn_name + fn.first_nations_id as id, + fn.name as first_nations_name FROM project_first_nation pfn LEFT OUTER JOIN @@ -260,17 +233,30 @@ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLSta WHERE pfn.project_id = ${projectId} GROUP BY + fn.first_nations_id, fn.name; `; +}; - defaultLog.debug({ - label: 'getIndigenousPartnershipsByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +/** + * SQL query to get project stakeholder partnerships. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getStakeholderPartnershipsByProjectSQL = (projectId: number): SQLStatement | null => { + if (!projectId) { + return null; + } - return sqlStatement; + return SQL` + SELECT + name as partnership_name + FROM + stakeholder_partnership + WHERE + project_id = ${projectId}; + `; }; /** @@ -280,13 +266,11 @@ export const getIndigenousPartnershipsByProjectSQL = (projectId: number): SQLSta * @returns {SQLStatement} sql query object */ export const getProjectPermitsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getProjectPermitsSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT number, type @@ -295,13 +279,101 @@ export const getProjectPermitsSQL = (projectId: number): SQLStatement | null => WHERE project_id = ${projectId} `; +}; + +/** + * SQL query to get project location. + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getLocationByProjectSQL = (projectId: number): SQLStatement | null => { + if (!projectId) { + return null; + } + + return SQL` + SELECT + p.location_description, + p.geojson as geometry, + p.revision_count + FROM + project p + WHERE + p.project_id = ${projectId} + GROUP BY + p.location_description, + p.geojson, + p.revision_count; + `; +}; - defaultLog.debug({ - label: 'getProjectPermitsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +/** + * SQL query to get project activities. + * + * @param {string} projectId + * @returns {SQLStatement} sql query object + */ - return sqlStatement; +export const getActivitiesByProjectSQL = (projectId: number): SQLStatement | null => { + if (!projectId) { + return null; + } + + return SQL` + SELECT + activity_id + from + project_activity + where project_id = ${projectId}; + `; +}; + +/** + * SQL query to get funding source data + * + * @param {number} projectId + * @returns {SQLStatement} sql query object + */ +export const getFundingSourceByProjectSQL = (projectId: number): SQLStatement | null => { + if (!projectId) { + return null; + } + + return SQL` + SELECT + pfs.project_funding_source_id as id, + fs.funding_source_id as agency_id, + pfs.funding_amount::numeric::int, + pfs.funding_start_date as start_date, + pfs.funding_end_date as end_date, + iac.investment_action_category_id as investment_action_category, + iac.name as investment_action_category_name, + fs.name as agency_name, + pfs.funding_source_project_id as agency_project_id, + pfs.revision_count as revision_count + FROM + project_funding_source as pfs + LEFT OUTER JOIN + investment_action_category as iac + ON + pfs.investment_action_category_id = iac.investment_action_category_id + LEFT OUTER JOIN + funding_source as fs + ON + iac.funding_source_id = fs.funding_source_id + WHERE + pfs.project_id = ${projectId} + GROUP BY + pfs.project_funding_source_id, + fs.funding_source_id, + pfs.funding_source_project_id, + pfs.funding_amount, + pfs.funding_start_date, + pfs.funding_end_date, + iac.investment_action_category_id, + iac.name, + fs.name, + pfs.revision_count + `; }; diff --git a/api/src/queries/project/project-view-update-queries.test.ts b/api/src/queries/project/project-view-update-queries.test.ts deleted file mode 100644 index 18e9def7e7..0000000000 --- a/api/src/queries/project/project-view-update-queries.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from 'chai'; -import { describe } from 'mocha'; -import { - getStakeholderPartnershipsByProjectSQL, - getActivitiesByProjectSQL, - getLocationByProjectSQL, - getFundingSourceByProjectSQL -} from './project-view-update-queries'; - -describe('getLocationByProjectSQL', () => { - it('Null projectId', () => { - const response = getLocationByProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('valid projectId', () => { - const response = getLocationByProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getStakeholderPartnershipsByProjectSQL', () => { - it('Null projectId', () => { - const response = getStakeholderPartnershipsByProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('valid projectId', () => { - const response = getStakeholderPartnershipsByProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getActivitiesByProjectSQL', () => { - it('Null projectId', () => { - const response = getActivitiesByProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('valid projectId', () => { - const response = getActivitiesByProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getFundingSourceByProjectSQL', () => { - it('returns null response when null projectId provided', () => { - const response = getFundingSourceByProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns non null response when valid projectId provided', () => { - const response = getFundingSourceByProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); diff --git a/api/src/queries/project/project-view-update-queries.ts b/api/src/queries/project/project-view-update-queries.ts deleted file mode 100644 index 5b478379c8..0000000000 --- a/api/src/queries/project/project-view-update-queries.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/project/project-create-queries'); - -/** - * SQL query to get project location. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getLocationByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getLocationByProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - p.location_description, - p.geojson as geometry, - p.revision_count - FROM - project p - WHERE - p.project_id = ${projectId} - GROUP BY - p.location_description, - p.geojson, - p.revision_count; - `; - - defaultLog.debug({ - label: 'getLocationByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get project stakeholder partnerships. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getStakeholderPartnershipsByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getStakeholderPartnershipsByProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - name as sp_name - FROM - stakeholder_partnership - WHERE - project_id = ${projectId}; - `; - - defaultLog.debug({ - label: 'getStakeholderPartnershipsByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get project activities. - * - * @param {string} projectId - * @returns {SQLStatement} sql query object - */ - -export const getActivitiesByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getActivitiesByProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - activity_id - from - project_activity - where project_id = ${projectId}; - `; - - defaultLog.debug({ - label: 'getActivitiesByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get funding source data - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getFundingSourceByProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getFundingSourceByProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - pfs.project_funding_source_id as id, - fs.funding_source_id as agency_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date as start_date, - pfs.funding_end_date as end_date, - iac.investment_action_category_id as investment_action_category, - iac.name as investment_action_category_name, - fs.name as agency_name, - pfs.funding_source_project_id as agency_project_id, - pfs.revision_count as revision_count - FROM - project_funding_source as pfs - LEFT OUTER JOIN - investment_action_category as iac - ON - pfs.investment_action_category_id = iac.investment_action_category_id - LEFT OUTER JOIN - funding_source as fs - ON - iac.funding_source_id = fs.funding_source_id - WHERE - pfs.project_id = ${projectId} - GROUP BY - pfs.project_funding_source_id, - fs.funding_source_id, - pfs.funding_source_project_id, - pfs.funding_amount, - pfs.funding_start_date, - pfs.funding_end_date, - iac.investment_action_category_id, - iac.name, - fs.name, - pfs.revision_count - `; - - defaultLog.debug({ - label: 'getFundingSourceByProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; diff --git a/api/src/queries/public/index.ts b/api/src/queries/public/index.ts new file mode 100644 index 0000000000..44f5fb2f5d --- /dev/null +++ b/api/src/queries/public/index.ts @@ -0,0 +1,4 @@ +import * as project from './project-queries'; +import * as search from './search-queries'; + +export default { ...project, ...search }; diff --git a/api/src/queries/public/project-queries.test.ts b/api/src/queries/public/project-queries.test.ts index 2162fccdf3..4e95239b51 100644 --- a/api/src/queries/public/project-queries.test.ts +++ b/api/src/queries/public/project-queries.test.ts @@ -1,49 +1,17 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { - getPublicProjectSQL, - getPublicProjectPermitsSQL, - getLocationByPublicProjectSQL, getActivitiesByPublicProjectSQL, - getIUCNActionClassificationByPublicProjectSQL, - getFundingSourceByPublicProjectSQL, - getIndigenousPartnershipsByPublicProjectSQL, - getStakeholderPartnershipsByPublicProjectSQL, - getPublicProjectListSQL, - getPublicProjectAttachmentsSQL, + getProjectReportAuthorsSQL, getPublicProjectAttachmentS3KeySQL, + getPublicProjectAttachmentsSQL, + getPublicProjectListSQL, + getPublicProjectReportAttachmentS3KeySQL, + getPublicProjectReportAttachmentSQL, getPublicProjectReportAttachmentsSQL, - getPublicProjectReportAttachmentS3KeySQL + getPublicProjectSQL } from './project-queries'; -describe('getPublicProjectReportAttachmentS3KeySQL', () => { - it('returns null when null attachment id param provided', () => { - const response = getPublicProjectReportAttachmentS3KeySQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns non null response when valid attachment id param provided', () => { - const response = getPublicProjectReportAttachmentS3KeySQL(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getPublicProjectReportAttachmentsSQL', () => { - it('returns null when null project id param provided', () => { - const response = getPublicProjectReportAttachmentsSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns non null response when valid project id param provided', () => { - const response = getPublicProjectReportAttachmentsSQL(1); - - expect(response).to.not.be.null; - }); -}); - describe('getPublicProjectSQL', () => { it('returns null when null project id param provided', () => { const response = getPublicProjectSQL((null as unknown) as number); @@ -58,135 +26,125 @@ describe('getPublicProjectSQL', () => { }); }); -describe('getPublicProjectPermitsSQL', () => { +describe('getActivitiesByPublicProjectSQL', () => { it('returns null when null project id param provided', () => { - const response = getPublicProjectPermitsSQL((null as unknown) as number); + const response = getActivitiesByPublicProjectSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns non null response when valid project id param provided', () => { - const response = getPublicProjectPermitsSQL(1); + const response = getActivitiesByPublicProjectSQL(1); expect(response).to.not.be.null; }); }); -describe('getLocationByPublicProjectSQL', () => { - it('returns null when null project id param provided', () => { - const response = getLocationByPublicProjectSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns non null response when valid project id param provided', () => { - const response = getLocationByPublicProjectSQL(1); +describe('getPublicProjectListSQL', () => { + it('returns non null response when called', () => { + const response = getPublicProjectListSQL(); expect(response).to.not.be.null; }); }); -describe('getActivitiesByPublicProjectSQL', () => { +describe('getPublicProjectAttachmentsSQL', () => { it('returns null when null project id param provided', () => { - const response = getActivitiesByPublicProjectSQL((null as unknown) as number); + const response = getPublicProjectAttachmentsSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns non null response when valid project id param provided', () => { - const response = getActivitiesByPublicProjectSQL(1); + const response = getPublicProjectAttachmentsSQL(1); expect(response).to.not.be.null; }); }); -describe('getIUCNActionClassificationByPublicProjectSQL', () => { +describe('getPublicProjectReportAttachmentsSQL', () => { it('returns null when null project id param provided', () => { - const response = getIUCNActionClassificationByPublicProjectSQL((null as unknown) as number); + const response = getPublicProjectReportAttachmentsSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns non null response when valid project id param provided', () => { - const response = getIUCNActionClassificationByPublicProjectSQL(1); + const response = getPublicProjectReportAttachmentsSQL(1); expect(response).to.not.be.null; }); }); -describe('getFundingSourceByPublicProjectSQL', () => { +describe('getPublicProjectReportAttachmentS3KeySQL', () => { it('returns null when null project id param provided', () => { - const response = getFundingSourceByPublicProjectSQL((null as unknown) as number); + const response = getPublicProjectReportAttachmentS3KeySQL((null as unknown) as number, 2); expect(response).to.be.null; }); - it('returns non null response when valid project id param provided', () => { - const response = getFundingSourceByPublicProjectSQL(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getIndigenousPartnershipsByPublicProjectSQL', () => { - it('returns null when null project id param provided', () => { - const response = getIndigenousPartnershipsByPublicProjectSQL((null as unknown) as number); + it('returns null when null attachment id param provided', () => { + const response = getPublicProjectReportAttachmentS3KeySQL(1, (null as unknown) as number); expect(response).to.be.null; }); - it('returns non null response when valid project id param provided', () => { - const response = getIndigenousPartnershipsByPublicProjectSQL(1); + it('returns non null response when valid attachment id param provided', () => { + const response = getPublicProjectReportAttachmentS3KeySQL(1, 2); expect(response).to.not.be.null; }); }); -describe('getStakeholderPartnershipsByPublicProjectSQL', () => { +describe('getPublicProjectAttachmentS3KeySQL', () => { it('returns null when null project id param provided', () => { - const response = getStakeholderPartnershipsByPublicProjectSQL((null as unknown) as number); + const response = getPublicProjectAttachmentS3KeySQL((null as unknown) as number, 2); expect(response).to.be.null; }); - it('returns non null response when valid project id param provided', () => { - const response = getStakeholderPartnershipsByPublicProjectSQL(1); + it('returns null when null attachment id param provided', () => { + const response = getPublicProjectAttachmentS3KeySQL(1, (null as unknown) as number); - expect(response).to.not.be.null; + expect(response).to.be.null; }); -}); -describe('getPublicProjectListSQL', () => { - it('returns non null response when called', () => { - const response = getPublicProjectListSQL(); + it('returns non null response when valid params provided', () => { + const response = getPublicProjectAttachmentS3KeySQL(1, 2); expect(response).to.not.be.null; }); }); -describe('getPublicProjectAttachmentsSQL', () => { +describe('getPublicProjectReportAttachmentSQL', () => { it('returns null when null project id param provided', () => { - const response = getPublicProjectAttachmentsSQL((null as unknown) as number); + const response = getPublicProjectReportAttachmentSQL((null as unknown) as number, 2); expect(response).to.be.null; }); - it('returns non null response when valid project id param provided', () => { - const response = getPublicProjectAttachmentsSQL(1); + it('returns null when null attachment id param provided', () => { + const response = getPublicProjectReportAttachmentSQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid params provided', () => { + const response = getPublicProjectReportAttachmentSQL(1, 2); expect(response).to.not.be.null; }); }); -describe('getPublicProjectAttachmentS3KeySQL', () => { - it('returns null when null attachment id param provided', () => { - const response = getPublicProjectAttachmentS3KeySQL((null as unknown) as number); +describe('getProjectReportAuthorSQL', () => { + it('returns null response when null projectReportAttachmentId provided', () => { + const response = getProjectReportAuthorsSQL((null as unknown) as number); expect(response).to.be.null; }); - it('returns non null response when valid attachment id param provided', () => { - const response = getPublicProjectAttachmentS3KeySQL(1); + it('returns non null response when valid projectReportAttachmentId provided', () => { + const response = getProjectReportAuthorsSQL(1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/public/project-queries.ts b/api/src/queries/public/project-queries.ts index 94239955b1..f07fddc990 100644 --- a/api/src/queries/public/project-queries.ts +++ b/api/src/queries/public/project-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/public/project-queries'); /** * SQL query to get a single public (published) project. @@ -10,28 +7,21 @@ const defaultLog = getLogger('queries/public/project-queries'); * @returns {SQLStatement} sql query object */ export const getPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT project.project_id as id, + project.project_type_id as pt_id, project_type.name as type, project.name, - project.objectives, project.location_description, project.start_date, project.end_date, project.caveats, project.comments, - project.coordinator_first_name, - project.coordinator_last_name, - project.coordinator_email_address, - project.coordinator_agency_name, - project.coordinator_public, project.geojson as geometry, project.publish_timestamp as publish_date from @@ -43,92 +33,6 @@ export const getPublicProjectSQL = (projectId: number): SQLStatement | null => { project.project_id = ${projectId} and project.publish_timestamp is not null; `; - - defaultLog.debug({ - label: 'getPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get permits associated to a public (published) project. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getPublicProjectPermitsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectPermitsSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - number, - type - FROM - permit as per - LEFT OUTER JOIN - project as p - ON - per.project_id = p.project_id - WHERE - per.project_id = ${projectId} - AND p.publish_timestamp is not null; - `; - - defaultLog.debug({ - label: 'getPublicProjectPermitsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get public (published) project location. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getLocationByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getLocationByPublicProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - p.location_description, - p.geojson as geometry, - p.revision_count - FROM - project p - WHERE - p.project_id = ${projectId} - AND p.publish_timestamp is not null - GROUP BY - p.location_description, - p.geojson, - p.revision_count; - `; - - defaultLog.debug({ - label: 'getLocationByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -139,240 +43,24 @@ export const getLocationByPublicProjectSQL = (projectId: number): SQLStatement | */ export const getActivitiesByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getActivitiesByPublicProjectSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - a.name + pa.activity_id from project_activity as pa LEFT OUTER JOIN project as p ON p.project_id = pa.project_id - LEFT OUTER JOIN - activity as a - ON - a.activity_id = pa.activity_id - where pa.project_id = ${projectId} - and p.publish_timestamp is not null; - `; - - defaultLog.debug({ - label: 'getActivitiesByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get IUCN action classifications for a public (published) project. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getIUCNActionClassificationByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIUCNActionClassificationByPublicProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - ical1c.name as classification, - ical2s.name as subClassification1, - ical3s.name as subClassification2 - FROM - project_iucn_action_classification as piac - LEFT OUTER JOIN - iucn_conservation_action_level_3_subclassification as ical3s - ON - piac.iucn_conservation_action_level_3_subclassification_id = ical3s.iucn_conservation_action_level_3_subclassification_id - LEFT OUTER JOIN - iucn_conservation_action_level_2_subclassification as ical2s - ON - ical3s.iucn_conservation_action_level_2_subclassification_id = ical2s.iucn_conservation_action_level_2_subclassification_id - LEFT OUTER JOIN - iucn_conservation_action_level_1_classification as ical1c - ON - ical2s.iucn_conservation_action_level_1_classification_id = ical1c.iucn_conservation_action_level_1_classification_id - LEFT OUTER JOIN - project as p - ON - piac.project_id = p.project_id - WHERE - piac.project_id = ${projectId} - AND - p.publish_timestamp is not null - GROUP BY - ical1c.name, - ical2s.name, - ical3s.name; - `; - - defaultLog.debug({ - label: 'getIUCNActionClassificationByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get funding source data for a public (published) project - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getFundingSourceByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getFundingSourceByPublicProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - pfs.project_funding_source_id as id, - fs.funding_source_id as agency_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date as start_date, - pfs.funding_end_date as end_date, - iac.investment_action_category_id as investment_action_category, - iac.name as investment_action_category_name, - fs.name as agency_name, - pfs.funding_source_project_id as agency_project_id, - pfs.revision_count as revision_count - FROM - project_funding_source as pfs - LEFT OUTER JOIN - investment_action_category as iac - ON - pfs.investment_action_category_id = iac.investment_action_category_id - LEFT OUTER JOIN - funding_source as fs - ON - iac.funding_source_id = fs.funding_source_id - LEFT OUTER JOIN - project as p - ON - pfs.project_id = p.project_id WHERE - pfs.project_id = ${projectId} - AND - p.publish_timestamp is not null - GROUP BY - pfs.project_funding_source_id, - fs.funding_source_id, - pfs.funding_source_project_id, - pfs.funding_amount, - pfs.funding_start_date, - pfs.funding_end_date, - iac.investment_action_category_id, - iac.name, - fs.name, - pfs.revision_count - `; - - defaultLog.debug({ - label: 'getFundingSourceByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get project indigenous partnerships for a public (published) project. - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getIndigenousPartnershipsByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getIndigenousPartnershipsByPublicProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - fn.name as fn_name - FROM - project_first_nation as pfn - LEFT OUTER JOIN - first_nations as fn - ON - pfn.first_nations_id = fn.first_nations_id - LEFT OUTER JOIN - project as p - ON - p.project_id = pfn.project_id - WHERE - pfn.project_id = ${projectId} - AND - p.publish_timestamp is not null - GROUP BY - fn.name; - `; - - defaultLog.debug({ - label: 'getIndigenousPartnershipsByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get project stakeholder partnerships for a public (published) project. - * - * @param {number} projectId - * @returns {SQLStatement} sql query object - */ -export const getStakeholderPartnershipsByPublicProjectSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getStakeholderPartnershipsByPublicProjectSQL', message: 'params', projectId }); - - if (!projectId) { - return null; - } - - const sqlStatement = SQL` - SELECT - sp.name as sp_name - FROM - stakeholder_partnership as sp - LEFT OUTER JOIN - project as p - ON - p.project_id = sp.project_id - WHERE - sp.project_id = ${projectId} + pa.project_id = ${projectId} AND p.publish_timestamp is not null; `; - - defaultLog.debug({ - label: 'getStakeholderPartnershipsByPublicProjectSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -381,15 +69,13 @@ export const getStakeholderPartnershipsByPublicProjectSQL = (projectId: number): * @returns {SQLStatement} sql query object */ export const getPublicProjectListSQL = (): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectListSQL', message: 'params' }); - - const sqlStatement = SQL` + return SQL` SELECT p.project_id as id, p.name, p.start_date, p.end_date, - p.coordinator_agency_name, + p.coordinator_agency_name as coordinator_agency, pt.name as project_type, string_agg(DISTINCT pp.number, ', ') as permits_list from @@ -400,9 +86,6 @@ export const getPublicProjectListSQL = (): SQLStatement | null => { on p.project_id = pp.project_id where p.publish_timestamp is not null - `; - - sqlStatement.append(SQL` group by p.project_id, p.name, @@ -410,16 +93,7 @@ export const getPublicProjectListSQL = (): SQLStatement | null => { p.end_date, p.coordinator_agency_name, pt.name; - `); - - defaultLog.debug({ - label: 'getPublicProjectListSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + `; }; /** @@ -429,13 +103,11 @@ export const getPublicProjectListSQL = (): SQLStatement | null => { * @returns {SQLStatement} sql query object */ export const getPublicProjectAttachmentsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectAttachmentsSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT pa.project_attachment_id as id, pa.file_name, @@ -455,15 +127,6 @@ export const getPublicProjectAttachmentsSQL = (projectId: number): SQLStatement and p.publish_timestamp is not null; `; - - defaultLog.debug({ - label: 'getPublicProjectAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -473,13 +136,11 @@ export const getPublicProjectAttachmentsSQL = (projectId: number): SQLStatement * @returns {SQLStatement} sql query object */ export const getPublicProjectReportAttachmentsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectReportAttachmentsSQL', message: 'params', projectId }); - if (!projectId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT pa.project_report_attachment_id as id, pa.file_name, @@ -498,79 +159,116 @@ export const getPublicProjectReportAttachmentsSQL = (projectId: number): SQLStat and p.publish_timestamp is not null; `; - - defaultLog.debug({ - label: 'getPublicProjectReportAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** * SQL query to get S3 key of an attachment for a single public (published) project. * + * @param {number} projectId * @param {number} attachmentId * @returns {SQLStatement} sql query object */ -export const getPublicProjectAttachmentS3KeySQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectAttachmentS3KeySQL', message: 'params', attachmentId }); - - if (!attachmentId) { +export const getPublicProjectAttachmentS3KeySQL = (projectId: number, attachmentId: number): SQLStatement | null => { + if (!projectId || !attachmentId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT CASE WHEN api_security_check(security_token,create_user) THEN key ELSE null END as key FROM project_attachment WHERE + project_id = ${projectId} + AND project_attachment_id = ${attachmentId}; `; - - defaultLog.debug({ - label: 'getPublicProjectAttachmentS3KeySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** * SQL query to get S3 key of a report attachment for a single public (published) project. * + * @param {number} projectId * @param {number} attachmentId * @returns {SQLStatement} sql query object */ -export const getPublicProjectReportAttachmentS3KeySQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicProjectReportAttachmentS3KeySQL', message: 'params', attachmentId }); - - if (!attachmentId) { +export const getPublicProjectReportAttachmentS3KeySQL = ( + projectId: number, + attachmentId: number +): SQLStatement | null => { + if (!projectId || !attachmentId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT CASE WHEN api_security_check(security_token,create_user) THEN key ELSE null END as key FROM project_report_attachment WHERE + project_id = ${projectId} + AND project_report_attachment_id = ${attachmentId}; `; +}; - defaultLog.debug({ - label: 'getPublicProjectReportAttachmentS3KeySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +/** + * Get the metadata fields of an unsecured project report attachment, for the specified `projectId` and `attachmentId`. + * + * @param {number} projectId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const getPublicProjectReportAttachmentSQL = (projectId: number, attachmentId: number): SQLStatement | null => { + if (!projectId || !attachmentId) { + return null; + } + + return SQL` + SELECT + project_report_attachment_id as attachment_id, + file_name, + title, + description, + year as year_published, + update_date, + create_date, + file_size, + CASE WHEN api_security_check(security_token,create_user) THEN key ELSE null + END as key, + security_token, + revision_count + FROM + project_report_attachment + where + project_report_attachment_id = ${attachmentId} + and + project_id = ${projectId} + `; +}; - return sqlStatement; +/** + * Get the metadata fields of project report attachment, for the specified `projectId` and `attachmentId`. + * + * @param {number} projectId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const getProjectReportAuthorsSQL = (projectReportAttachmentId: number): SQLStatement | null => { + if (!projectReportAttachmentId) { + return null; + } + + return SQL` + SELECT + project_report_author.* + FROM + project_report_author + where + project_report_attachment_id = ${projectReportAttachmentId} + `; }; diff --git a/api/src/queries/public/search-queries.ts b/api/src/queries/public/search-queries.ts index 39084c7ad4..d70adb3e11 100644 --- a/api/src/queries/public/search-queries.ts +++ b/api/src/queries/public/search-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/public/search-queries'); /** * SQL query to get public project geometries @@ -9,9 +6,7 @@ const defaultLog = getLogger('queries/public/search-queries'); * @returns {SQLStatement} sql query object */ export const getPublicSpatialSearchResultsSQL = (): SQLStatement | null => { - defaultLog.debug({ label: 'getPublicSpatialSearchResultsSQL', message: 'params' }); - - const sqlStatement = SQL` + return SQL` SELECT p.project_id as id, p.name, @@ -21,13 +16,4 @@ export const getPublicSpatialSearchResultsSQL = (): SQLStatement | null => { where p.publish_timestamp is not null; `; - - defaultLog.debug({ - label: 'getPublicSpatialSearchResultsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; diff --git a/api/src/queries/queries.ts b/api/src/queries/queries.ts new file mode 100644 index 0000000000..ef069829d4 --- /dev/null +++ b/api/src/queries/queries.ts @@ -0,0 +1,31 @@ +import administrativeActivity from './administrative-activity'; +import codes from './codes'; +import database from './database'; +import dwc from './dwc'; +import occurrence from './occurrence'; +import permit from './permit'; +import project from './project'; +import projectParticipation from './project-participation'; +import publicQueries from './public'; +import search from './search'; +import security from './security'; +import spatial from './spatial'; +import survey from './survey'; +import users from './users'; + +export const queries = { + administrativeActivity, + codes, + database, + dwc, + occurrence, + permit, + project, + projectParticipation, + public: publicQueries, + search, + security, + spatial, + survey, + users +}; diff --git a/api/src/queries/search/index.ts b/api/src/queries/search/index.ts new file mode 100644 index 0000000000..808a321e83 --- /dev/null +++ b/api/src/queries/search/index.ts @@ -0,0 +1,3 @@ +import * as search from './search-queries'; + +export default { ...search }; diff --git a/api/src/queries/search-queries.test.ts b/api/src/queries/search/search-queries.test.ts similarity index 100% rename from api/src/queries/search-queries.test.ts rename to api/src/queries/search/search-queries.test.ts diff --git a/api/src/queries/search-queries.ts b/api/src/queries/search/search-queries.ts similarity index 66% rename from api/src/queries/search-queries.ts rename to api/src/queries/search/search-queries.ts index e1f774477c..81e438b1f1 100644 --- a/api/src/queries/search-queries.ts +++ b/api/src/queries/search/search-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../utils/logger'; - -const defaultLog = getLogger('queries/search-queries'); /** * SQL query to get project geometries @@ -11,8 +8,6 @@ const defaultLog = getLogger('queries/search-queries'); * @returns {SQLStatement} sql query object */ export const getSpatialSearchResultsSQL = (isUserAdmin: boolean, systemUserId: number | null): SQLStatement | null => { - defaultLog.debug({ label: 'getSpatialSearchResultsSQL', message: 'params', isUserAdmin, systemUserId }); - if (!systemUserId) { return null; } @@ -34,12 +29,5 @@ export const getSpatialSearchResultsSQL = (isUserAdmin: boolean, systemUserId: n sqlStatement.append(SQL`;`); } - defaultLog.debug({ - label: 'getSpatialSearchResultsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/security/index.ts b/api/src/queries/security/index.ts new file mode 100644 index 0000000000..69055b0d84 --- /dev/null +++ b/api/src/queries/security/index.ts @@ -0,0 +1,3 @@ +import * as security from './security-queries'; + +export default { ...security }; diff --git a/api/src/queries/security/security-queries.test.ts b/api/src/queries/security/security-queries.test.ts index 7ed1b7571b..6e6f11efd7 100644 --- a/api/src/queries/security/security-queries.test.ts +++ b/api/src/queries/security/security-queries.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { unsecureAttachmentRecordSQL, secureAttachmentRecordSQL } from './security-queries'; +import { secureAttachmentRecordSQL, unsecureAttachmentRecordSQL } from './security-queries'; describe('unsecureAttachmentRecordSQL', () => { it('returns null when no tableName provided', () => { diff --git a/api/src/queries/security/security-queries.ts b/api/src/queries/security/security-queries.ts index 1f39052d9b..7e2d89034b 100644 --- a/api/src/queries/security/security-queries.ts +++ b/api/src/queries/security/security-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/security/security-queries'); /** * SQL query to unsecure an attachment record. @@ -11,21 +8,12 @@ const defaultLog = getLogger('queries/security/security-queries'); * @returns {SQLStatement} sql query object */ export const unsecureAttachmentRecordSQL = (tableName: string, securityToken: any): SQLStatement | null => { - defaultLog.debug({ label: 'unsecureAttachmentRecordSQL', message: 'params', tableName, securityToken }); - if (!securityToken || !tableName) { return null; } const sqlStatement: SQLStatement = SQL`select api_unsecure_attachment_record(${tableName}, ${securityToken})`; - defaultLog.debug({ - label: 'unsecureAttachmentRecordSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -42,20 +30,11 @@ export const secureAttachmentRecordSQL = ( tableName: string, projectId: number ): SQLStatement | null => { - defaultLog.debug({ label: 'secureAttachmentRecordSQL', message: 'params', attachmentId, tableName, projectId }); - if (!attachmentId || !tableName || !projectId) { return null; } const sqlStatement: SQLStatement = SQL`select api_secure_attachment_record(${attachmentId}, ${tableName}, ${projectId})`; - defaultLog.debug({ - label: 'secureAttachmentRecordSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/generate-geometry-collection.ts b/api/src/queries/spatial/generate-geometry-collection.ts similarity index 100% rename from api/src/queries/generate-geometry-collection.ts rename to api/src/queries/spatial/generate-geometry-collection.ts index 18f03fe355..415ae93852 100644 --- a/api/src/queries/generate-geometry-collection.ts +++ b/api/src/queries/spatial/generate-geometry-collection.ts @@ -1,5 +1,5 @@ -import { SQL, SQLStatement } from 'sql-template-strings'; import { Feature } from 'geojson'; +import { SQL, SQLStatement } from 'sql-template-strings'; /* Function to generate the SQL for insertion of a geometry collection diff --git a/api/src/queries/spatial/index.ts b/api/src/queries/spatial/index.ts new file mode 100644 index 0000000000..94662a6e17 --- /dev/null +++ b/api/src/queries/spatial/index.ts @@ -0,0 +1,3 @@ +import * as generateGeometryCollection from './generate-geometry-collection'; + +export default { ...generateGeometryCollection }; diff --git a/api/src/queries/survey/index.ts b/api/src/queries/survey/index.ts new file mode 100644 index 0000000000..c7ff747f23 --- /dev/null +++ b/api/src/queries/survey/index.ts @@ -0,0 +1,19 @@ +import * as surveyAttachments from './survey-attachments-queries'; +import * as surveyCreate from './survey-create-queries'; +import * as surveyDelete from './survey-delete-queries'; +import * as surveyOccurrence from './survey-occurrence-queries'; +import * as surveySummary from './survey-summary-queries'; +import * as surveyUpdate from './survey-update-queries'; +import * as surveyView from './survey-view-queries'; +import * as surveyViewUpdate from './survey-view-update-queries'; + +export default { + ...surveyAttachments, + ...surveyCreate, + ...surveyDelete, + ...surveyOccurrence, + ...surveySummary, + ...surveyUpdate, + ...surveyView, + ...surveyViewUpdate +}; diff --git a/api/src/queries/survey/survey-attachments-queries.test.ts b/api/src/queries/survey/survey-attachments-queries.test.ts index 41cdc02120..c0f7341bfe 100644 --- a/api/src/queries/survey/survey-attachments-queries.test.ts +++ b/api/src/queries/survey/survey-attachments-queries.test.ts @@ -1,18 +1,51 @@ import { expect } from 'chai'; import { describe } from 'mocha'; +import { IReportAttachmentAuthor, PutReportAttachmentMetadata } from '../../models/project-survey-attachments'; import { - getSurveyAttachmentsSQL, deleteSurveyAttachmentSQL, - getSurveyAttachmentS3KeySQL, - postSurveyAttachmentSQL, + deleteSurveyReportAttachmentAuthorsSQL, + deleteSurveyReportAttachmentSQL, getSurveyAttachmentByFileNameSQL, - putSurveyAttachmentSQL, + getSurveyAttachmentS3KeySQL, + getSurveyAttachmentsSQL, + getSurveyReportAttachmentByFileNameSQL, + getSurveyReportAttachmentS3KeySQL, + getSurveyReportAttachmentSQL, getSurveyReportAttachmentsSQL, - deleteSurveyReportAttachmentSQL, + getSurveyReportAuthorsSQL, + insertSurveyReportAttachmentAuthorSQL, + postSurveyAttachmentSQL, postSurveyReportAttachmentSQL, - putSurveyReportAttachmentSQL + putSurveyAttachmentSQL, + putSurveyReportAttachmentSQL, + updateSurveyReportAttachmentMetadataSQL } from './survey-attachments-queries'; +const post_sample_attachment_meta = { + title: 'title', + year_published: 2000, + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ], + description: 'description' +}; + +const put_sample_attachment_meta = { + title: 'title', + year_published: 2000, + authors: [ + { + first_name: 'John', + last_name: 'Smith' + } + ], + description: 'description', + revision_count: 0 +}; + describe('getSurveyAttachmentsSQL', () => { it('returns null response when null surveyId provided', () => { const response = getSurveyAttachmentsSQL((null as unknown) as number); @@ -43,57 +76,97 @@ describe('deleteSurveyAttachmentSQL', () => { describe('putSurveyReportAttachmentSQL', () => { it('returns null response when null fileName provided', () => { - const response = putSurveyReportAttachmentSQL(1, (null as unknown) as string); + const response = putSurveyReportAttachmentSQL(1, (null as unknown) as string, put_sample_attachment_meta); expect(response).to.be.null; }); it('returns null response when null surveyId provided', () => { - const response = putSurveyReportAttachmentSQL((null as unknown) as number, 'name'); + const response = putSurveyReportAttachmentSQL((null as unknown) as number, 'name', put_sample_attachment_meta); expect(response).to.be.null; }); it('returns non null response when valid params provided', () => { - const response = putSurveyReportAttachmentSQL(1, 'name'); + const response = putSurveyReportAttachmentSQL(1, 'name', put_sample_attachment_meta); expect(response).to.not.be.null; }); }); -describe('postSurveyReportAttachmentSQL', () => { - it('returns null response when null fileName provided', () => { - const response = postSurveyReportAttachmentSQL((null as unknown) as string, 30, 1, 2, 'key'); +describe('updateSurveyReportAttachmentMetadataSQL', () => { + it('returns null response when null surveyId provided', () => { + const response = updateSurveyReportAttachmentMetadataSQL( + (null as unknown) as number, + 1, + put_sample_attachment_meta + ); expect(response).to.be.null; }); - it('returns null response when null fileSize provided', () => { - const response = postSurveyReportAttachmentSQL('name', (null as unknown) as number, 1, 2, 'key'); + it('returns null response when null attachmentId provided', () => { + const response = updateSurveyReportAttachmentMetadataSQL( + 1, + (null as unknown) as number, + put_sample_attachment_meta + ); expect(response).to.be.null; }); - it('returns null response when null projectId provided', () => { - const response = postSurveyReportAttachmentSQL('name', 30, (null as unknown) as number, 2, 'key'); + it('returns null response when null metadata provided', () => { + const response = updateSurveyReportAttachmentMetadataSQL(1, 1, (null as unknown) as PutReportAttachmentMetadata); expect(response).to.be.null; }); - it('returns null response when null surveyId provided', () => { - const response = postSurveyReportAttachmentSQL('name', 30, 1, (null as unknown) as number, 'key'); + it('returns non null response when valid params provided', () => { + const response = updateSurveyReportAttachmentMetadataSQL(1, 2, put_sample_attachment_meta); + + expect(response).to.not.be.null; + }); +}); + +describe('postSurveyReportAttachmentSQL', () => { + it('returns null response when null fileName provided', () => { + const response = postSurveyReportAttachmentSQL( + (null as unknown) as string, + 30, + 1, + 'key', + post_sample_attachment_meta + ); expect(response).to.be.null; }); - it('returns null response when null key provided', () => { - const response = postSurveyReportAttachmentSQL('name', 30, 1, 2, (null as unknown) as string); + it('returns null response when null fileSize provided', () => { + const response = postSurveyReportAttachmentSQL( + 'name', + (null as unknown) as number, + 1, + 'key', + post_sample_attachment_meta + ); + + expect(response).to.be.null; + }); + + it('returns null response when null projectId provided', () => { + const response = postSurveyReportAttachmentSQL( + 'name', + 30, + (null as unknown) as number, + 'key', + post_sample_attachment_meta + ); expect(response).to.be.null; }); it('returns non null response when valid params provided', () => { - const response = postSurveyReportAttachmentSQL('name', 30, 1, 2, 'key'); + const response = postSurveyReportAttachmentSQL('name', 30, 1, 'key', post_sample_attachment_meta); expect(response).to.not.be.null; }); @@ -148,44 +221,44 @@ describe('getSurveyAttachmentS3KeySQL', () => { }); describe('postSurveyAttachmentSQL', () => { - it('returns null response when null projectId provided', () => { - const response = postSurveyAttachmentSQL('name', 20, 'type', (null as unknown) as number, 1, 'key'); + it('returns null response when null surveyId provided', () => { + const response = postSurveyAttachmentSQL('name', 20, 'type', (null as unknown) as number, 'key'); expect(response).to.be.null; }); it('returns null response when null fileName provided', () => { - const response = postSurveyAttachmentSQL((null as unknown) as string, 20, 'type', 1, 1, 'key'); + const response = postSurveyAttachmentSQL((null as unknown) as string, 20, 'type', 1, 'key'); expect(response).to.be.null; }); it('returns null response when null fileSize provided', () => { - const response = postSurveyAttachmentSQL('name', (null as unknown) as number, 'type', 1, 1, 'key'); + const response = postSurveyAttachmentSQL('name', (null as unknown) as number, 'type', 1, 'key'); expect(response).to.be.null; }); it('returns null response when null surveyId provided', () => { - const response = postSurveyAttachmentSQL('name', 20, 'type', 1, (null as unknown) as number, 'key'); + const response = postSurveyAttachmentSQL('name', 20, 'type', 1, (null as unknown) as string); expect(response).to.be.null; }); it('returns null response when null key provided', () => { - const response = postSurveyAttachmentSQL('name', 20, 'type', 1, 3, (null as unknown) as any); + const response = postSurveyAttachmentSQL('name', 20, 'type', 1, (null as unknown) as string); expect(response).to.be.null; }); it('returns null response when null fileType provided', () => { - const response = postSurveyAttachmentSQL('name', 20, (null as unknown) as string, 1, 2, 'key'); + const response = postSurveyAttachmentSQL('name', 20, (null as unknown) as string, 1, 'key'); expect(response).to.be.null; }); it('returns non null response when valid params provided', () => { - const response = postSurveyAttachmentSQL('name', 20, 'type', 1, 1, 'key'); + const response = postSurveyAttachmentSQL('name', 20, 'type', 1, 'key'); expect(response).to.not.be.null; }); @@ -211,6 +284,26 @@ describe('getSurveyAttachmentByFileNameSQL', () => { }); }); +describe('getSurveyReportAttachmentByFileNameSQL', () => { + it('returns null response when null surveyId provided', () => { + const response = getSurveyReportAttachmentByFileNameSQL((null as unknown) as number, 'name'); + + expect(response).to.be.null; + }); + + it('returns null response when null fileName provided', () => { + const response = getSurveyReportAttachmentByFileNameSQL(1, (null as unknown) as string); + + expect(response).to.be.null; + }); + + it('returns non null response when valid surveyId and fileName provided', () => { + const response = getSurveyReportAttachmentByFileNameSQL(1, 'name'); + + expect(response).to.not.be.null; + }); +}); + describe('putSurveyAttachmentSQL', () => { it('returns null response when null surveyId provided', () => { const response = putSurveyAttachmentSQL((null as unknown) as number, 'name', 'type'); @@ -236,3 +329,103 @@ describe('putSurveyAttachmentSQL', () => { expect(response).to.not.be.null; }); }); + +describe('insertSurveyReportAttachmentAuthorSQL', () => { + const report_attachment_author: IReportAttachmentAuthor = { + first_name: 'John', + last_name: 'Smith' + }; + it('returns null response when null attachmentId provided', () => { + const response = insertSurveyReportAttachmentAuthorSQL((null as unknown) as number, report_attachment_author); + + expect(response).to.be.null; + }); + + it('returns null response when null report author provided', () => { + const response = insertSurveyReportAttachmentAuthorSQL(1, (null as unknown) as IReportAttachmentAuthor); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmmentId and null report author are provided', () => { + const response = insertSurveyReportAttachmentAuthorSQL( + (null as unknown) as number, + (null as unknown) as IReportAttachmentAuthor + ); + expect(response).to.be.null; + }); + + it('returns not null response when valid parameters are provided', () => { + const response = insertSurveyReportAttachmentAuthorSQL(1, report_attachment_author); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteSurveyReportAttachmentAuthorsSQL', () => { + it('returns null response when null attachmentId provided', () => { + const response = deleteSurveyReportAttachmentAuthorsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns not null response when valid params are provided', () => { + const response = deleteSurveyReportAttachmentAuthorsSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyReportAuthorSQL', () => { + it('returns null response when null projectReportAttachmentId provided', () => { + const response = getSurveyReportAuthorsSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectReportAttachmentId provided', () => { + const response = getSurveyReportAuthorsSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyReportAttachmentSQL', () => { + it('returns null response when null surveyId provided', () => { + const response = getSurveyReportAttachmentSQL((null as unknown) as number, 1); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmentId provided', () => { + const response = getSurveyReportAttachmentSQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectReportAttachmentId provided', () => { + const response = getSurveyReportAttachmentSQL(1, 2); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyReportAttachmentS3KeySQL', () => { + it('returns null response when null surveyId provided', () => { + const response = getSurveyReportAttachmentS3KeySQL((null as unknown) as number, 1); + + expect(response).to.be.null; + }); + + it('returns null response when null attachmentId provided', () => { + const response = getSurveyReportAttachmentS3KeySQL(1, (null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid projectReportAttachmentId provided', () => { + const response = getSurveyReportAttachmentS3KeySQL(1, 2); + + expect(response).to.not.be.null; + }); +}); diff --git a/api/src/queries/survey/survey-attachments-queries.ts b/api/src/queries/survey/survey-attachments-queries.ts index dc02473e72..c934a7aef6 100644 --- a/api/src/queries/survey/survey-attachments-queries.ts +++ b/api/src/queries/survey/survey-attachments-queries.ts @@ -1,7 +1,9 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/survey/survey-attachments-queries'); +import { + IReportAttachmentAuthor, + PostReportAttachmentMetadata, + PutReportAttachmentMetadata +} from '../../models/project-survey-attachments'; /** * SQL query to get attachments for a single survey. @@ -10,8 +12,6 @@ const defaultLog = getLogger('queries/survey/survey-attachments-queries'); * @returns {SQLStatement} sql query object */ export const getSurveyAttachmentsSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveyAttachmentsSQL', message: 'params', surveyId }); - if (!surveyId) { return null; } @@ -32,25 +32,16 @@ export const getSurveyAttachmentsSQL = (surveyId: number): SQLStatement | null = survey_id = ${surveyId}; `; - defaultLog.debug({ - label: 'getSurveyAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; /** - * SQL query to get report attachments for a single survey. + * SQL query to get the list of report attachments for a single survey. * * @param {number} surveyId * @returns {SQLStatement} sql query object */ export const getSurveyReportAttachmentsSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveyReportAttachmentsSQL', message: 'params', surveyId }); - if (!surveyId) { return null; } @@ -70,12 +61,39 @@ export const getSurveyReportAttachmentsSQL = (surveyId: number): SQLStatement | survey_id = ${surveyId}; `; - defaultLog.debug({ - label: 'getSurveyReportAttachmentsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to get report attachments for a single survey. + * + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const getSurveyReportAttachmentSQL = (surveyId: number, attachmentId: number): SQLStatement | null => { + if (!surveyId || !attachmentId) { + return null; + } + const sqlStatement: SQLStatement = SQL` + SELECT + survey_report_attachment_id as attachment_id, + file_name, + title, + description, + year as year_published, + update_date, + create_date, + file_size, + key, + security_token, + revision_count + FROM + survey_report_attachment + where + survey_report_attachment_id = ${attachmentId} + and + survey_id = ${surveyId} + `; return sqlStatement; }; @@ -87,8 +105,6 @@ export const getSurveyReportAttachmentsSQL = (surveyId: number): SQLStatement | * @returns {SQLStatement} sql query object */ export const deleteSurveyAttachmentSQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteSurveyAttachmentSQL', message: 'params', attachmentId }); - if (!attachmentId) { return null; } @@ -102,13 +118,6 @@ export const deleteSurveyAttachmentSQL = (attachmentId: number): SQLStatement | key; `; - defaultLog.debug({ - label: 'deleteSurveyAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -119,8 +128,6 @@ export const deleteSurveyAttachmentSQL = (attachmentId: number): SQLStatement | * @returns {SQLStatement} sql query object */ export const deleteSurveyReportAttachmentSQL = (attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'deleteSurveyReportAttachmentSQL', message: 'params', attachmentId }); - if (!attachmentId) { return null; } @@ -134,13 +141,6 @@ export const deleteSurveyReportAttachmentSQL = (attachmentId: number): SQLStatem key; `; - defaultLog.debug({ - label: 'deleteSurveyReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -152,8 +152,6 @@ export const deleteSurveyReportAttachmentSQL = (attachmentId: number): SQLStatem * @returns {SQLStatement} sql query object */ export const getSurveyAttachmentS3KeySQL = (surveyId: number, attachmentId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveyAttachmentS3KeySQL', message: 'params', surveyId }); - if (!surveyId || !attachmentId) { return null; } @@ -169,12 +167,31 @@ export const getSurveyAttachmentS3KeySQL = (surveyId: number, attachmentId: numb survey_attachment_id = ${attachmentId}; `; - defaultLog.debug({ - label: 'getSurveyAttachmentS3KeySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to get S3 key of a report attachment for a single survey. + * + * @param {number} surveyId + * @param {number} attachmentId + * @returns {SQLStatement} sql query object + */ +export const getSurveyReportAttachmentS3KeySQL = (surveyId: number, attachmentId: number): SQLStatement | null => { + if (!surveyId || !attachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + key + FROM + survey_report_attachment + WHERE + survey_id = ${surveyId} + AND + survey_report_attachment_id = ${attachmentId}; + `; return sqlStatement; }; @@ -185,7 +202,6 @@ export const getSurveyAttachmentS3KeySQL = (surveyId: number, attachmentId: numb * @param {string} fileName * @param {number} fileSize * @param {string} fileType - * @param {number} projectId * @param {number} surveyId * @param {string} key to use in s3 * @returns {SQLStatement} sql query object @@ -194,22 +210,10 @@ export const postSurveyAttachmentSQL = ( fileName: string, fileSize: number, fileType: string, - projectId: number, surveyId: number, key: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'postSurveyAttachmentSQL', - message: 'params', - fileName, - fileSize, - fileType, - projectId, - surveyId, - key - }); - - if (!fileName || !fileSize || !fileType || !projectId || !surveyId || !key) { + if (!fileName || !fileSize || !fileType || !surveyId || !key) { return null; } @@ -228,16 +232,10 @@ export const postSurveyAttachmentSQL = ( ${key} ) RETURNING - survey_attachment_id as id; + survey_attachment_id as id, + revision_count; `; - defaultLog.debug({ - label: 'postSurveyAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -254,58 +252,37 @@ export const postSurveyAttachmentSQL = ( export const postSurveyReportAttachmentSQL = ( fileName: string, fileSize: number, - projectId: number, surveyId: number, - key: string + key: string, + attachmentMeta: PostReportAttachmentMetadata ): SQLStatement | null => { - defaultLog.debug({ - label: 'postSurveyReportAttachmentSQL', - message: 'params', - fileName, - fileSize, - projectId, - surveyId, - key - }); - - if (!fileName || !fileSize || !projectId || !surveyId || !key) { + if (!fileName || !fileSize || !surveyId || !key) { return null; } - // TODO: Replace hard-coded title, year and description - const title = 'Test Report'; - const year = '2021'; - const description = 'Test description'; - const sqlStatement: SQLStatement = SQL` INSERT INTO survey_report_attachment ( survey_id, file_name, - file_size, - key, title, year, - description + description, + file_size, + key ) VALUES ( ${surveyId}, ${fileName}, + ${attachmentMeta.title}, + ${attachmentMeta.year_published}, + ${attachmentMeta.description}, ${fileSize}, - ${key}, - ${title}, - ${year}, - ${description} + ${key} ) RETURNING - survey_report_attachment_id as id; + survey_report_attachment_id as id, + revision_count; `; - defaultLog.debug({ - label: 'postSurveyReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -317,8 +294,6 @@ export const postSurveyReportAttachmentSQL = ( * @returns {SQLStatement} sql query object */ export const getSurveyAttachmentByFileNameSQL = (surveyId: number, fileName: string): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveyAttachmentByFileNameSQL', message: 'params', surveyId }); - if (!surveyId || !fileName) { return null; } @@ -338,12 +313,35 @@ export const getSurveyAttachmentByFileNameSQL = (surveyId: number, fileName: str file_name = ${fileName}; `; - defaultLog.debug({ - label: 'getSurveyAttachmentByFileNameSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to get an attachment for a single survey by survey id and filename. + * + * @param {number} surveyId + * @param {string} fileName + * @returns {SQLStatement} sql query object + */ +export const getSurveyReportAttachmentByFileNameSQL = (surveyId: number, fileName: string): SQLStatement | null => { + if (!surveyId || !fileName) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + survey_report_attachment_id as id, + file_name, + update_date, + create_date, + file_size + from + survey_report_attachment + where + survey_id = ${surveyId} + and + file_name = ${fileName}; + `; return sqlStatement; }; @@ -357,8 +355,6 @@ export const getSurveyAttachmentByFileNameSQL = (surveyId: number, fileName: str * @returns {SQLStatement} sql query object */ export const putSurveyAttachmentSQL = (surveyId: number, fileName: string, fileType: string): SQLStatement | null => { - defaultLog.debug({ label: 'putSurveyAttachmentSQL', message: 'params', surveyId, fileName, fileType }); - if (!surveyId || !fileName || !fileType) { return null; } @@ -372,15 +368,12 @@ export const putSurveyAttachmentSQL = (surveyId: number, fileName: string, fileT WHERE file_name = ${fileName} AND - survey_id = ${surveyId}; - `; + survey_id = ${surveyId} + RETURNING + survey_attachment_id as id, + revision_count; - defaultLog.debug({ - label: 'putSurveyAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + `; return sqlStatement; }; @@ -392,9 +385,11 @@ export const putSurveyAttachmentSQL = (surveyId: number, fileName: string, fileT * @param {string} fileName * @returns {SQLStatement} sql query object */ -export const putSurveyReportAttachmentSQL = (surveyId: number, fileName: string): SQLStatement | null => { - defaultLog.debug({ label: 'putSurveyReportAttachmentSQL', message: 'params', surveyId, fileName }); - +export const putSurveyReportAttachmentSQL = ( + surveyId: number, + fileName: string, + attachmentMeta: PutReportAttachmentMetadata +): SQLStatement | null => { if (!surveyId || !fileName) { return null; } @@ -403,19 +398,135 @@ export const putSurveyReportAttachmentSQL = (surveyId: number, fileName: string) UPDATE survey_report_attachment SET - file_name = ${fileName} + file_name = ${fileName}, + title = ${attachmentMeta.title}, + year = ${attachmentMeta.year_published}, + description = ${attachmentMeta.description} WHERE file_name = ${fileName} AND - survey_id = ${surveyId}; + survey_id = ${surveyId} + RETURNING + survey_report_attachment_id as id, + revision_count; + `; + + return sqlStatement; +}; + +export interface ReportAttachmentMeta { + title: string; + description: string; + yearPublished: string; +} + +/** + * Update the metadata fields of survey report attachment, for the specified `surveyId` and `attachmentId`. + * + * @param {number} surveyId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const updateSurveyReportAttachmentMetadataSQL = ( + surveyId: number, + attachmentId: number, + metadata: PutReportAttachmentMetadata +): SQLStatement | null => { + if (!surveyId || !attachmentId || !metadata) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + UPDATE + survey_report_attachment + SET + title = ${metadata.title}, + year = ${metadata.year_published}, + description = ${metadata.description} + WHERE + survey_id = ${surveyId} + AND + survey_report_attachment_id = ${attachmentId} + AND + revision_count = ${metadata.revision_count}; `; - defaultLog.debug({ - label: 'putSurveyReportAttachmentSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * Insert a new survey report attachment author record, for the specified `attachmentId` + * + * @param {number} attachmentId + * @param {IReportAttachmentAuthor} author + * @return {*} {(SQLStatement | null)} + */ +export const insertSurveyReportAttachmentAuthorSQL = ( + attachmentId: number, + author: IReportAttachmentAuthor +): SQLStatement | null => { + if (!attachmentId || !author) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + INSERT INTO survey_report_author ( + survey_report_attachment_id, + first_name, + last_name + ) VALUES ( + ${attachmentId}, + ${author.first_name}, + ${author.last_name} + ); + `; + + return sqlStatement; +}; + +/** + * Delete all project report attachment author records, for the specified `attachmentId`. + * + * @param {number} attachmentId + * @return {*} {(SQLStatement | null)} + */ +export const deleteSurveyReportAttachmentAuthorsSQL = (attachmentId: number): SQLStatement | null => { + if (!attachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + DELETE FROM + survey_report_author + WHERE + survey_report_attachment_id = ${attachmentId}; + `; + + return sqlStatement; +}; + +/** + * Get the metadata fields of survey report attachment, for the specified `surveyId` and `attachmentId`. + * + * @param {number} surveyId + * @param {number} attachmentId + * @param {PutReportAttachmentMetadata} metadata + * @return {*} {(SQLStatement | null)} + */ +export const getSurveyReportAuthorsSQL = (surveyReportAttachmentId: number): SQLStatement | null => { + if (!surveyReportAttachmentId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + SELECT + survey_report_author.* + FROM + survey_report_author + where + survey_report_attachment_id = ${surveyReportAttachmentId} + `; return sqlStatement; }; diff --git a/api/src/queries/survey/survey-create-queries.test.ts b/api/src/queries/survey/survey-create-queries.test.ts index bca00a6525..84fcb0385c 100644 --- a/api/src/queries/survey/survey-create-queries.test.ts +++ b/api/src/queries/survey/survey-create-queries.test.ts @@ -2,12 +2,12 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PostSurveyObject, PostSurveyProprietorData } from '../../models/survey-create'; import { - postFocalSpeciesSQL, + insertSurveyFundingSourceSQL, postAncillarySpeciesSQL, - postSurveyProprietorSQL, - postSurveySQL, + postFocalSpeciesSQL, postNewSurveyPermitSQL, - insertSurveyFundingSourceSQL + postSurveyProprietorSQL, + postSurveySQL } from './survey-create-queries'; describe('postSurveySQL', () => { diff --git a/api/src/queries/survey/survey-create-queries.ts b/api/src/queries/survey/survey-create-queries.ts index 8f42aa9641..c805dbfea7 100644 --- a/api/src/queries/survey/survey-create-queries.ts +++ b/api/src/queries/survey/survey-create-queries.ts @@ -1,9 +1,6 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; import { PostSurveyObject, PostSurveyProprietorData } from '../../models/survey-create'; -import { generateGeometryCollectionSQL } from '../generate-geometry-collection'; - -const defaultLog = getLogger('queries/survey/survey-create-queries'); +import { queries } from '../queries'; /** * SQL query to insert a survey row. @@ -13,13 +10,6 @@ const defaultLog = getLogger('queries/survey/survey-create-queries'); * @returns {SQLStatement} sql query object */ export const postSurveySQL = (projectId: number, survey: PostSurveyObject): SQLStatement | null => { - defaultLog.debug({ - label: 'postSurveySQL', - message: 'params', - projectId, - survey - }); - if (!projectId || !survey) { return null; } @@ -28,30 +18,36 @@ export const postSurveySQL = (projectId: number, survey: PostSurveyObject): SQLS INSERT INTO survey ( project_id, name, - objectives, + additional_details, + ecological_season_id, + intended_outcome_id, start_date, end_date, lead_first_name, lead_last_name, location_name, geojson, - common_survey_methodology_id, + field_method_id, + surveyed_all_areas, geography ) VALUES ( ${projectId}, ${survey.survey_name}, - ${survey.survey_purpose}, + ${survey.additional_details}, + ${survey.ecological_season_id}, + ${survey.intended_outcome_id}, ${survey.start_date}, ${survey.end_date}, ${survey.biologist_first_name}, ${survey.biologist_last_name}, ${survey.survey_area_name}, ${JSON.stringify(survey.geometry)}, - ${survey.common_survey_methodology_id} + ${survey.field_method_id}, + ${survey.surveyed_all_areas} `; if (survey.geometry && survey.geometry.length) { - const geometryCollectionSQL = generateGeometryCollectionSQL(survey.geometry); + const geometryCollectionSQL = queries.spatial.generateGeometryCollectionSQL(survey.geometry); sqlStatement.append(SQL` ,public.geography( @@ -76,13 +72,6 @@ export const postSurveySQL = (projectId: number, survey: PostSurveyObject): SQLS survey_id as id; `); - defaultLog.debug({ - label: 'postSurveySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -97,13 +86,6 @@ export const postSurveyProprietorSQL = ( surveyId: number, survey_proprietor: PostSurveyProprietorData ): SQLStatement | null => { - defaultLog.debug({ - label: 'postSurveyProprietorSQL', - message: 'params', - surveyId, - survey_proprietor - }); - if (!surveyId || !survey_proprietor) { return null; } @@ -128,13 +110,6 @@ export const postSurveyProprietorSQL = ( survey_proprietor_id as id; `; - defaultLog.debug({ - label: 'postSurveyProprietorSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -146,13 +121,6 @@ export const postSurveyProprietorSQL = ( * @returns {SQLStatement} sql query object */ export const insertSurveyFundingSourceSQL = (surveyId: number, fundingSourceId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveyFundingSourceSQL', - message: 'params', - surveyId, - fundingSourceId - }); - if (!surveyId || !fundingSourceId) { return null; } @@ -167,13 +135,6 @@ export const insertSurveyFundingSourceSQL = (surveyId: number, fundingSourceId: ); `; - defaultLog.debug({ - label: 'insertSurveyFundingSourceSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -194,16 +155,6 @@ export const postNewSurveyPermitSQL = ( permitNumber: string, permitType: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'postNewSurveyPermitSQL', - message: 'params', - systemUserId, - projectId, - surveyId, - permitNumber, - permitType - }); - if (!systemUserId || !projectId || !surveyId || !permitNumber || !permitType) { return null; } @@ -224,13 +175,6 @@ export const postNewSurveyPermitSQL = ( ); `; - defaultLog.debug({ - label: 'postNewSurveyPermitSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -242,8 +186,6 @@ export const postNewSurveyPermitSQL = ( * @returns {SQLStatement} sql query object */ export const postFocalSpeciesSQL = (speciesId: number, surveyId: number): SQLStatement | null => { - defaultLog.debug({ label: 'postFocalSpeciesSQL', message: 'params', speciesId, surveyId }); - if (!speciesId || !surveyId) { return null; } @@ -260,13 +202,6 @@ export const postFocalSpeciesSQL = (speciesId: number, surveyId: number): SQLSta ) RETURNING study_species_id as id; `; - defaultLog.debug({ - label: 'postFocalSpeciesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -278,8 +213,6 @@ export const postFocalSpeciesSQL = (speciesId: number, surveyId: number): SQLSta * @returns {SQLStatement} sql query object */ export const postAncillarySpeciesSQL = (speciesId: number, surveyId: number): SQLStatement | null => { - defaultLog.debug({ label: 'postAncillarySpeciesSQL', message: 'params', speciesId, surveyId }); - if (!speciesId || !surveyId) { return null; } @@ -296,12 +229,30 @@ export const postAncillarySpeciesSQL = (speciesId: number, surveyId: number): SQ ) RETURNING study_species_id as id; `; - defaultLog.debug({ - label: 'postAncillarySpeciesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to insert a ancillary species row into the study_species table. + * + * @param {number} speciesId + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const postVantageCodesSQL = (vantageCodeId: number, surveyId: number): SQLStatement | null => { + if (!vantageCodeId || !surveyId) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + INSERT INTO survey_vantage ( + vantage_id, + survey_id + ) VALUES ( + ${vantageCodeId}, + ${surveyId} + ) RETURNING survey_vantage_id as id; + `; return sqlStatement; }; diff --git a/api/src/queries/survey/survey-delete-queries.test.ts b/api/src/queries/survey/survey-delete-queries.test.ts index dbd09138fa..d2c162e28d 100644 --- a/api/src/queries/survey/survey-delete-queries.test.ts +++ b/api/src/queries/survey/survey-delete-queries.test.ts @@ -1,12 +1,12 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { - deleteFocalSpeciesSQL, deleteAncillarySpeciesSQL, - deleteSurveyProprietorSQL, - deleteSurveySQL, + deleteFocalSpeciesSQL, + deleteSurveyFundingSourceByProjectFundingSourceIdSQL, deleteSurveyFundingSourcesBySurveyIdSQL, - deleteSurveyFundingSourceByProjectFundingSourceIdSQL + deleteSurveyProprietorSQL, + deleteSurveySQL } from './survey-delete-queries'; describe('deleteFocalSpeciesSQL', () => { diff --git a/api/src/queries/survey/survey-delete-queries.ts b/api/src/queries/survey/survey-delete-queries.ts index 63123f6003..03734159b5 100644 --- a/api/src/queries/survey/survey-delete-queries.ts +++ b/api/src/queries/survey/survey-delete-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/survey/survey-delete-queries'); /** * SQL query to delete survey funding sources rows based on survey id. @@ -10,12 +7,6 @@ const defaultLog = getLogger('queries/survey/survey-delete-queries'); * @returns {SQLStatement} sql query object */ export const deleteSurveyFundingSourcesBySurveyIdSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSurveyFundingSourcesBySurveyIdSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } @@ -27,13 +18,6 @@ export const deleteSurveyFundingSourcesBySurveyIdSQL = (surveyId: number): SQLSt survey_id = ${surveyId}; `; - defaultLog.debug({ - label: 'deleteSurveyFundingSourcesBySurveyIdSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -46,12 +30,6 @@ export const deleteSurveyFundingSourcesBySurveyIdSQL = (surveyId: number): SQLSt export const deleteSurveyFundingSourceByProjectFundingSourceIdSQL = ( projectFundingSourceId: number | undefined ): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL', - message: 'params', - projectFundingSourceId - }); - if (!projectFundingSourceId) { return null; } @@ -63,13 +41,6 @@ export const deleteSurveyFundingSourceByProjectFundingSourceIdSQL = ( project_funding_source_id = ${projectFundingSourceId}; `; - defaultLog.debug({ - label: 'deleteSurveyFundingSourceByProjectFundingSourceIdSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -80,12 +51,6 @@ export const deleteSurveyFundingSourceByProjectFundingSourceIdSQL = ( * @returns {SQLStatement} sql query object */ export const deleteFocalSpeciesSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteFocalSpeciesSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } @@ -99,13 +64,6 @@ export const deleteFocalSpeciesSQL = (surveyId: number): SQLStatement | null => is_focal; `; - defaultLog.debug({ - label: 'deleteFocalSpeciesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -116,12 +74,6 @@ export const deleteFocalSpeciesSQL = (surveyId: number): SQLStatement | null => * @returns {SQLStatement} sql query object */ export const deleteAncillarySpeciesSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteAncillarySpeciesSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } @@ -135,13 +87,6 @@ export const deleteAncillarySpeciesSQL = (surveyId: number): SQLStatement | null is_focal is FALSE; `; - defaultLog.debug({ - label: 'deleteAncillarySpeciesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -153,13 +98,6 @@ export const deleteAncillarySpeciesSQL = (surveyId: number): SQLStatement | null * @returns {SQLStatement} sql query object */ export const deleteSurveyProprietorSQL = (surveyId: number, surveyProprietorId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSurveyProprietorSQL', - message: 'params', - surveyId, - surveyProprietorId - }); - if ((!surveyId && surveyId !== 0) || (!surveyProprietorId && surveyProprietorId !== 0)) { return null; } @@ -173,13 +111,6 @@ export const deleteSurveyProprietorSQL = (surveyId: number, surveyProprietorId: survey_id = ${surveyId} `; - defaultLog.debug({ - label: 'deleteSurveyProprietorSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -190,24 +121,33 @@ export const deleteSurveyProprietorSQL = (surveyId: number, surveyProprietorId: * @returns {SQLStatement} sql query object */ export const deleteSurveySQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSurveySQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } const sqlStatement: SQLStatement = SQL`call api_delete_survey(${surveyId})`; - defaultLog.debug({ - label: 'deleteSurveySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); + return sqlStatement; +}; + +/** + * SQL query to delete survey proprietor rows. + * + * @param {number} surveyId + * @param {number} surveyProprietorId + * @returns {SQLStatement} sql query object + */ +export const deleteSurveyVantageCodesSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId && surveyId !== 0) { + return null; + } + + const sqlStatement: SQLStatement = SQL` + DELETE + from survey_vantage + WHERE + survey_id = ${surveyId} + `; return sqlStatement; }; diff --git a/api/src/queries/survey/survey-occurrence-queries.test.ts b/api/src/queries/survey/survey-occurrence-queries.test.ts index 8e119a0724..dd3efd5a0d 100644 --- a/api/src/queries/survey/survey-occurrence-queries.test.ts +++ b/api/src/queries/survey/survey-occurrence-queries.test.ts @@ -1,17 +1,16 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { + deleteOccurrenceSubmissionSQL, deleteSurveyOccurrencesSQL, getLatestSurveyOccurrenceSubmissionSQL, getOccurrenceSubmissionMessagesSQL, getSurveyOccurrenceSubmissionSQL, - getTemplateMethodologySpeciesIdSQLStatement, - getTemplateMethodologySpeciesSQL, - insertSurveyOccurrenceSubmissionSQL, + getTemplateMethodologySpeciesRecordSQL, insertOccurrenceSubmissionMessageSQL, insertOccurrenceSubmissionStatusSQL, - updateSurveyOccurrenceSubmissionSQL, - deleteOccurrenceSubmissionSQL + insertSurveyOccurrenceSubmissionSQL, + updateSurveyOccurrenceSubmissionSQL } from './survey-occurrence-queries'; describe('insertSurveyOccurrenceSubmissionSQL', () => { @@ -19,8 +18,7 @@ describe('insertSurveyOccurrenceSubmissionSQL', () => { const response = insertSurveyOccurrenceSubmissionSQL({ surveyId: (null as unknown) as number, source: 'fileSource', - inputKey: 'fileKey', - templateMethodologyId: 1 + inputKey: 'fileKey' }); expect(response).to.be.null; @@ -30,32 +28,19 @@ describe('insertSurveyOccurrenceSubmissionSQL', () => { const response = insertSurveyOccurrenceSubmissionSQL({ surveyId: 1, source: (null as unknown) as string, - inputKey: 'fileKey', - templateMethodologyId: 1 + inputKey: 'fileKey' }); expect(response).to.be.null; }); - it('returns non null response when null templateMethodologyId provided', () => { - const response = insertSurveyOccurrenceSubmissionSQL({ - surveyId: 1, - source: 'fileSource', - inputKey: 'fileKey', - templateMethodologyId: null - }); - - expect(response).to.not.be.null; - }); - it('returns non null response when all valid params provided without inputKey', () => { const response = insertSurveyOccurrenceSubmissionSQL({ surveyId: 1, source: 'fileSource', inputFileName: 'inputFileName', outputFileName: 'outputFileName', - outputKey: 'outputfileKey', - templateMethodologyId: 1 + outputKey: 'outputfileKey' }); expect(response).to.not.be.null; @@ -68,8 +53,7 @@ describe('insertSurveyOccurrenceSubmissionSQL', () => { inputFileName: 'inputFileName', inputKey: 'inputfileKey', outputFileName: 'outputFileName', - outputKey: 'outputfileKey', - templateMethodologyId: 1 + outputKey: 'outputfileKey' }); expect(response).to.not.be.null; @@ -260,29 +244,21 @@ describe('getOccurrenceSubmissionMessagesSQL', () => { }); }); -describe('getTemplateMethodologySpeciesIdSQLStatement', () => { - it('returns null response when null surveyId provided', () => { - const response = getTemplateMethodologySpeciesIdSQLStatement((null as unknown) as number); +describe('getTemplateMethodologySpeciesRecordSQL', () => { + it('returns null response when null methodologyId provided', () => { + const response = getTemplateMethodologySpeciesRecordSQL((null as unknown) as number, 1); expect(response).to.be.null; }); - it('returns non null response when valid params provided', () => { - const response = getTemplateMethodologySpeciesIdSQLStatement(1); - - expect(response).to.not.be.null; - }); -}); - -describe('getTemplateMethodologySpeciesSQL', () => { - it('returns null response when null occurrenceId provided', () => { - const response = getTemplateMethodologySpeciesSQL((null as unknown) as number); + it('returns null response when null templateId provided', () => { + const response = getTemplateMethodologySpeciesRecordSQL(1, (null as unknown) as number); expect(response).to.be.null; }); it('returns non null response when valid params provided', () => { - const response = getTemplateMethodologySpeciesSQL(1); + const response = getTemplateMethodologySpeciesRecordSQL(1, 1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/survey/survey-occurrence-queries.ts b/api/src/queries/survey/survey-occurrence-queries.ts index 06e327ed21..1c765557e5 100644 --- a/api/src/queries/survey/survey-occurrence-queries.ts +++ b/api/src/queries/survey/survey-occurrence-queries.ts @@ -1,5 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; import { AppendSQLColumn, appendSQLColumns, @@ -9,8 +8,6 @@ import { appendSQLValues } from '../../utils/sql-utils'; -const defaultLog = getLogger('queries/survey/survey-occurrence-queries'); - /** * SQL query to insert a survey occurrence submission row. * @@ -23,18 +20,11 @@ const defaultLog = getLogger('queries/survey/survey-occurrence-queries'); export const insertSurveyOccurrenceSubmissionSQL = (data: { surveyId: number; source: string; - templateMethodologyId: number | null; inputFileName?: string; inputKey?: string; outputFileName?: string; outputKey?: string; }): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveyOccurrenceSubmissionSQL', - message: 'params', - data - }); - if (!data || !data.surveyId || !data.source) { return null; } @@ -68,7 +58,6 @@ export const insertSurveyOccurrenceSubmissionSQL = (data: { INSERT INTO occurrence_submission ( survey_id, source, - template_methodology_species_id, event_timestamp, `; @@ -78,7 +67,6 @@ export const insertSurveyOccurrenceSubmissionSQL = (data: { ) VALUES ( ${data.surveyId}, ${data.source}, - ${data.templateMethodologyId}, now(), `); @@ -90,57 +78,6 @@ export const insertSurveyOccurrenceSubmissionSQL = (data: { occurrence_submission_id as id; `); - defaultLog.debug({ - label: 'insertSurveyOccurrenceSubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to get a template methodology species id. - * - * @param {number} surveyId - * @param {string} source - * @param {string} inputKey - * @return {*} {(SQLStatement | null)} - */ -export const getTemplateMethodologySpeciesIdSQLStatement = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getTemplateMethodologySpeciesIdSQLStatement', - message: 'params', - surveyId - }); - - if (!surveyId) { - return null; - } - - const sqlStatement: SQLStatement = SQL` - SELECT - tms.template_methodology_species_id - FROM - template_methodology_species tms - LEFT OUTER JOIN - template t on tms.template_id = t.template_id - LEFT OUTER JOIN - common_survey_methodology csm on tms.common_survey_methodology_id = csm.common_survey_methodology_id - LEFT OUTER JOIN - survey s on csm.common_survey_methodology_id = s.common_survey_methodology_id - WHERE - s.survey_id = ${surveyId}; - `; - - defaultLog.debug({ - label: 'getTemplateMethodologySpeciesIdSQLStatement', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -162,12 +99,6 @@ export const updateSurveyOccurrenceSubmissionSQL = (data: { outputFileName?: string; outputKey?: string; }): SQLStatement | null => { - defaultLog.debug({ - label: 'updateSurveyOccurrenceSubmissionSQL', - message: 'params', - data - }); - if (!data.submissionId || (!data.inputFileName && !data.inputKey && !data.outputFileName && !data.outputKey)) { return null; } @@ -205,13 +136,6 @@ export const updateSurveyOccurrenceSubmissionSQL = (data: { RETURNING occurrence_submission_id as id; `); - defaultLog.debug({ - label: 'updateSurveyOccurrenceSubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -222,17 +146,11 @@ export const updateSurveyOccurrenceSubmissionSQL = (data: { * @returns {SQLStatement} sql query object */ export const getLatestSurveyOccurrenceSubmissionSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getLatestSurveyOccurrenceSubmissionSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT os.occurrence_submission_id as id, os.survey_id, @@ -273,15 +191,6 @@ export const getLatestSurveyOccurrenceSubmissionSQL = (surveyId: number): SQLSta LIMIT 1 ; `; - - defaultLog.debug({ - label: 'getLatestSurveyOccurrenceSubmission', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -291,31 +200,16 @@ export const getLatestSurveyOccurrenceSubmissionSQL = (surveyId: number): SQLSta * @return {*} {(SQLStatement | null)} */ export const deleteSurveyOccurrencesSQL = (occurrenceSubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSurveyOccurrencesSQL', - message: 'params', - occurrenceSubmissionId - }); - if (!occurrenceSubmissionId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` DELETE FROM occurrence WHERE occurrence_submission_id = ${occurrenceSubmissionId}; `; - - defaultLog.debug({ - label: 'deleteSurveyOccurrencesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -325,13 +219,11 @@ export const deleteSurveyOccurrencesSQL = (occurrenceSubmissionId: number): SQLS * @returns {SQLStatement} sql query object */ export const getSurveyOccurrenceSubmissionSQL = (occurrenceSubmissionId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveyOccurrenceSubmissionSQL', message: 'params', occurrenceSubmissionId }); - if (!occurrenceSubmissionId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT * FROM @@ -339,15 +231,6 @@ export const getSurveyOccurrenceSubmissionSQL = (occurrenceSubmissionId: number) WHERE occurrence_submission_id = ${occurrenceSubmissionId}; `; - - defaultLog.debug({ - label: 'getSurveyOccurrenceSubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -357,30 +240,15 @@ export const getSurveyOccurrenceSubmissionSQL = (occurrenceSubmissionId: number) * @returns {SQLStatement} sql query object */ export const deleteOccurrenceSubmissionSQL = (occurrenceSubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteOccurrenceSubmissionSQL', - message: 'params', - occurrenceSubmissionId - }); - if (!occurrenceSubmissionId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` UPDATE occurrence_submission SET delete_timestamp = now() WHERE occurrence_submission_id = ${occurrenceSubmissionId}; `; - - defaultLog.debug({ - label: 'deleteOccurrenceSubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -394,18 +262,11 @@ export const insertOccurrenceSubmissionStatusSQL = ( occurrenceSubmissionId: number, submissionStatusType: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveySubmissionStatusSQL', - message: 'params', - occurrenceSubmissionId, - submissionStatusType - }); - if (!occurrenceSubmissionId || !submissionStatusType) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO submission_status ( occurrence_submission_id, submission_status_type_id, @@ -425,15 +286,6 @@ export const insertOccurrenceSubmissionStatusSQL = ( RETURNING submission_status_id as id; `; - - defaultLog.debug({ - label: 'insertSurveySubmissionStatusSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -450,20 +302,11 @@ export const insertOccurrenceSubmissionMessageSQL = ( submissionMessage: string, errorCode: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'insertOccurrenceSubmissionMessageSQL', - message: 'params', - submissionStatusId, - submissionMessageType, - submissionMessage, - errorCode - }); - if (!submissionStatusId || !submissionMessageType || !submissionMessage || !errorCode) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO submission_message ( submission_status_id, submission_message_type_id, @@ -485,15 +328,6 @@ export const insertOccurrenceSubmissionMessageSQL = ( RETURNING submission_message_id; `; - - defaultLog.debug({ - label: 'insertSurveySubmissionMessageSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -503,17 +337,11 @@ export const insertOccurrenceSubmissionMessageSQL = ( * @returns {SQLStatement} sql query object */ export const getOccurrenceSubmissionMessagesSQL = (occurrenceSubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getOccurrenceSubmissionMessagesSQL', - message: 'params', - occurrenceSubmissionId - }); - if (!occurrenceSubmissionId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT sm.submission_message_id as id, smt.name as type, @@ -546,53 +374,31 @@ export const getOccurrenceSubmissionMessagesSQL = (occurrenceSubmissionId: numbe os.occurrence_submission_id = ${occurrenceSubmissionId} ORDER BY sm.submission_message_id; `; - - defaultLog.debug({ - label: 'getOccurrenceSubmissionMessagesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** - * SQL query to get a template_methodology_species record for a submission based on the occurrence_submission_id. + * SQL query to get a template methodology species id. * - * @param {number} occurrenceId - * @returns {SQLStatement} sql query object + * @param {number} fieldMethodId + * @param {number} templateId + * @return {*} {(SQLStatement | null)} */ -export const getTemplateMethodologySpeciesSQL = (occurrenceId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getTemplateMethodologySpeciesSQL', - message: 'params', - occurrenceId - }); - - if (!occurrenceId) { +export const getTemplateMethodologySpeciesRecordSQL = ( + fieldMethodId: number, + templateId: number +): SQLStatement | null => { + if (!fieldMethodId || !templateId) { return null; } - const sqlStatement = SQL` - SELECT - tms.* + return SQL` + SELECT * FROM - occurrence_submission os - LEFT OUTER JOIN - template_methodology_species tms on os.template_methodology_species_id = tms.template_methodology_species_id - LEFT OUTER JOIN - template t on tms.template_id = t.template_id + template_methodology_species tms WHERE - os.occurrence_submission_id = ${occurrenceId}; - `; - - defaultLog.debug({ - label: 'getTemplateMethodologySpeciesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + tms.field_method_id = ${fieldMethodId} + AND + tms.template_id = ${templateId} + ; + `; }; diff --git a/api/src/queries/survey/survey-summary-queries.test.ts b/api/src/queries/survey/survey-summary-queries.test.ts index ebb95f8a1c..05d4d722a7 100644 --- a/api/src/queries/survey/survey-summary-queries.test.ts +++ b/api/src/queries/survey/survey-summary-queries.test.ts @@ -2,14 +2,14 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { PostSummaryDetails } from '../../models/summaryresults-create'; import { - insertSurveySummarySubmissionSQL, + deleteSummarySubmissionSQL, getLatestSurveySummarySubmissionSQL, - updateSurveySummarySubmissionWithKeySQL, + getSummarySubmissionMessagesSQL, getSurveySummarySubmissionSQL, insertSurveySummaryDetailsSQL, - deleteSummarySubmissionSQL, insertSurveySummarySubmissionMessageSQL, - getSummarySubmissionMessagesSQL + insertSurveySummarySubmissionSQL, + updateSurveySummarySubmissionWithKeySQL } from './survey-summary-queries'; describe('deleteSummarySubmissionSQL', () => { diff --git a/api/src/queries/survey/survey-summary-queries.ts b/api/src/queries/survey/survey-summary-queries.ts index 55a97f6be7..8543501d68 100644 --- a/api/src/queries/survey/survey-summary-queries.ts +++ b/api/src/queries/survey/survey-summary-queries.ts @@ -1,8 +1,5 @@ -import { PostSummaryDetails } from '../../models/summaryresults-create'; import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/survey/survey-summary-queries'); +import { PostSummaryDetails } from '../../models/summaryresults-create'; /** * SQL query to insert a survey summary submission row. @@ -17,17 +14,11 @@ export const insertSurveySummarySubmissionSQL = ( source: string, file_name: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveySummarySubmissionSQL', - message: 'params', - surveyId - }); - if (!surveyId || !source || !file_name) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO survey_summary_submission ( survey_id, source, @@ -41,15 +32,6 @@ export const insertSurveySummarySubmissionSQL = ( ) RETURNING survey_summary_submission_id as id; `; - - defaultLog.debug({ - label: 'insertSurveySummaryResultsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -59,56 +41,41 @@ export const insertSurveySummarySubmissionSQL = ( * @returns {SQLStatement} sql query object */ export const getLatestSurveySummarySubmissionSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getLatestSurveySummaryResultsSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } - const sqlStatement = SQL` - SELECT - sss.survey_summary_submission_id as id, - sss.key, - sss.file_name, - sss.delete_timestamp, - sssm.submission_message_type_id, - sssm.message, - ssmt.name as submission_message_type_name, - ssmt.summary_submission_message_class_id, - ssmc.name as submission_message_class_name - FROM - survey_summary_submission as sss - LEFT OUTER JOIN - survey_summary_submission_message as sssm - ON - sss.survey_summary_submission_id = sssm.survey_summary_submission_id - LEFT OUTER JOIN - summary_submission_message_type as ssmt - ON - sssm.submission_message_type_id = ssmt.submission_message_type_id - LEFT OUTER JOIN - summary_submission_message_class as ssmc - ON - ssmt.summary_submission_message_class_id = ssmc.summary_submission_message_class_id - WHERE - sss.survey_id = ${surveyId} - ORDER BY - sss.event_timestamp DESC - LIMIT 1; - `; - - defaultLog.debug({ - label: 'getLatestSurveySummaryResultsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + return SQL` + SELECT + sss.survey_summary_submission_id as id, + sss.key, + sss.file_name, + sss.delete_timestamp, + sssm.submission_message_type_id, + sssm.message, + ssmt.name as submission_message_type_name, + ssmt.summary_submission_message_class_id, + ssmc.name as submission_message_class_name + FROM + survey_summary_submission as sss + LEFT OUTER JOIN + survey_summary_submission_message as sssm + ON + sss.survey_summary_submission_id = sssm.survey_summary_submission_id + LEFT OUTER JOIN + summary_submission_message_type as ssmt + ON + sssm.submission_message_type_id = ssmt.submission_message_type_id + LEFT OUTER JOIN + summary_submission_message_class as ssmc + ON + ssmt.summary_submission_message_class_id = ssmc.summary_submission_message_class_id + WHERE + sss.survey_id = ${surveyId} + ORDER BY + sss.event_timestamp DESC + LIMIT 1; + `; }; /** @@ -118,30 +85,15 @@ export const getLatestSurveySummarySubmissionSQL = (surveyId: number): SQLStatem * @returns {SQLStatement} sql query object */ export const deleteSummarySubmissionSQL = (summarySubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'deleteSummarySubmissionSQL', - message: 'params', - summarySubmissionId - }); - if (!summarySubmissionId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` UPDATE survey_summary_submission SET delete_timestamp = now() WHERE survey_summary_submission_id = ${summarySubmissionId}; `; - - defaultLog.debug({ - label: 'deleteSummarySubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -155,18 +107,11 @@ export const updateSurveySummarySubmissionWithKeySQL = ( summarySubmissionId: number, key: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'updateSurveySummarySubmissionWithKeySQL', - message: 'params', - summarySubmissionId, - key - }); - if (!summarySubmissionId || !key) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` UPDATE survey_summary_submission SET key= ${key} @@ -174,15 +119,6 @@ export const updateSurveySummarySubmissionWithKeySQL = ( survey_summary_submission_id = ${summarySubmissionId} RETURNING survey_summary_submission_id as id; `; - - defaultLog.debug({ - label: 'updateSurveySummarySubmissionWithKeySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -192,13 +128,11 @@ export const updateSurveySummarySubmissionWithKeySQL = ( * @returns {SQLStatement} sql query object */ export const getSurveySummarySubmissionSQL = (summarySubmissionId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getSurveySummarySubmissionSQL', message: 'params', summarySubmissionId }); - if (!summarySubmissionId) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` SELECT * FROM @@ -206,15 +140,6 @@ export const getSurveySummarySubmissionSQL = (summarySubmissionId: number): SQLS WHERE survey_summary_submission_id = ${summarySubmissionId}; `; - - defaultLog.debug({ - label: 'getSurveySummarySubmissionSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -228,17 +153,11 @@ export const insertSurveySummaryDetailsSQL = ( summarySubmissionId: number, summaryDetails: PostSummaryDetails ): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveySummarySubmissionSQL', - message: 'params', - summarySubmissionId - }); - if (!summarySubmissionId || !summaryDetails) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO survey_summary_detail ( survey_summary_submission_id, study_area_id, @@ -276,15 +195,6 @@ export const insertSurveySummaryDetailsSQL = ( ) RETURNING survey_summary_detail_id as id; `; - - defaultLog.debug({ - label: 'insertSurveySummaryResultsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -302,20 +212,11 @@ export const insertSurveySummarySubmissionMessageSQL = ( summarySubmissionMessage: string, errorCode: string ): SQLStatement | null => { - defaultLog.debug({ - label: 'insertSurveySummarySubmissionMessageSQL', - message: 'params', - summarySubmissionId, - summarySubmissionMessageType, - summarySubmissionMessage, - errorCode - }); - if (!summarySubmissionId || !summarySubmissionMessageType || !summarySubmissionMessage || !errorCode) { return null; } - const sqlStatement: SQLStatement = SQL` + return SQL` INSERT INTO survey_summary_submission_message ( survey_summary_submission_id, submission_message_type_id, @@ -337,15 +238,6 @@ export const insertSurveySummarySubmissionMessageSQL = ( RETURNING submission_message_id; `; - - defaultLog.debug({ - label: 'insertSurveySummarySubmissionMessageSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -355,48 +247,33 @@ export const insertSurveySummarySubmissionMessageSQL = ( * @returns {SQLStatement} sql query object */ export const getSummarySubmissionMessagesSQL = (summarySubmissionId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSummarySubmissionMessagesSQL', - message: 'params', - summarySubmissionId - }); - if (!summarySubmissionId) { return null; } - const sqlStatement = SQL` - SELECT - sssm.submission_message_id as id, - sssm.message, - ssmt.name as type, - ssmc.name as class - FROM - survey_summary_submission as sss - LEFT OUTER JOIN - survey_summary_submission_message as sssm - ON - sssm.survey_summary_submission_id = sss.survey_summary_submission_id - LEFT OUTER JOIN - summary_submission_message_type as ssmt - ON - ssmt.submission_message_type_id = sssm.submission_message_type_id - LEFT OUTER JOIN - summary_submission_message_class as ssmc - ON - ssmc.summary_submission_message_class_id = ssmt.summary_submission_message_class_id - WHERE - sss.survey_summary_submission_id = ${summarySubmissionId} - ORDER BY - sssm.submission_message_id; - `; - - defaultLog.debug({ - label: 'getOccurrenceSubmissionMessagesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; + return SQL` + SELECT + sssm.submission_message_id as id, + sssm.message, + ssmt.name as type, + ssmc.name as class + FROM + survey_summary_submission as sss + LEFT OUTER JOIN + survey_summary_submission_message as sssm + ON + sssm.survey_summary_submission_id = sss.survey_summary_submission_id + LEFT OUTER JOIN + summary_submission_message_type as ssmt + ON + ssmt.submission_message_type_id = sssm.submission_message_type_id + LEFT OUTER JOIN + summary_submission_message_class as ssmc + ON + ssmc.summary_submission_message_class_id = ssmt.summary_submission_message_class_id + WHERE + sss.survey_summary_submission_id = ${summarySubmissionId} + ORDER BY + sssm.submission_message_id; + `; }; diff --git a/api/src/queries/survey/survey-update-queries.test.ts b/api/src/queries/survey/survey-update-queries.test.ts index 453c23726e..d21c6b13ce 100644 --- a/api/src/queries/survey/survey-update-queries.test.ts +++ b/api/src/queries/survey/survey-update-queries.test.ts @@ -3,20 +3,17 @@ import { describe } from 'mocha'; import { PutSurveyDetailsData, PutSurveyProprietorData } from '../../models/survey-update'; import { putNewSurveyPermitNumberSQL, - unassociatePermitFromSurveySQL, putSurveyDetailsSQL, putSurveyProprietorSQL, + unassociatePermitFromSurveySQL, updateSurveyPublishStatusSQL } from './survey-update-queries'; -import { getSurveyDetailsForUpdateSQL } from './survey-view-update-queries'; describe('putSurveyDetailsSQL', () => { const surveyData: PutSurveyDetailsData = { name: 'test', - objectives: 'objectives', focal_species: [1, 2], ancillary_species: [3, 4], - common_survey_methodology_id: 1, start_date: '2020/04/04', end_date: '2020/05/05', lead_first_name: 'first', @@ -113,20 +110,6 @@ describe('putNewSurveyPermitNumberSQL', () => { }); }); -describe('getSurveyForUpdateSQL', () => { - it('returns null when no surveyId provided', () => { - const response = getSurveyDetailsForUpdateSQL((null as unknown) as number); - - expect(response).to.be.null; - }); - - it('returns sql statement when valid params provided', () => { - const response = getSurveyDetailsForUpdateSQL(1); - - expect(response).to.not.be.null; - }); -}); - describe('putSurveyProprietorSQL', () => { it('returns null when surveyId is falsey value not equal to 0', () => { const response = putSurveyProprietorSQL((null as unknown) as number, { prt_id: 1 } as PutSurveyProprietorData); diff --git a/api/src/queries/survey/survey-update-queries.ts b/api/src/queries/survey/survey-update-queries.ts index 215ad45860..a26309be9a 100644 --- a/api/src/queries/survey/survey-update-queries.ts +++ b/api/src/queries/survey/survey-update-queries.ts @@ -1,9 +1,10 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { PutSurveyDetailsData, PutSurveyProprietorData } from '../../models/survey-update'; -import { getLogger } from '../../utils/logger'; -import { generateGeometryCollectionSQL } from '../generate-geometry-collection'; - -const defaultLog = getLogger('queries/survey/survey-update-queries'); +import { + PutSurveyDetailsData, + PutSurveyProprietorData, + PutSurveyPurposeAndMethodologyData +} from '../../models/survey-update'; +import { queries } from '../queries'; /** * SQL query to update a permit row based on an old survey association. @@ -13,32 +14,17 @@ const defaultLog = getLogger('queries/survey/survey-update-queries'); * @returns {SQLStatement} sql query object */ export const unassociatePermitFromSurveySQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'unassociatePermitFromSurveySQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` UPDATE permit SET survey_id = ${null} WHERE survey_id = ${surveyId}; `; - - defaultLog.debug({ - label: 'unassociatePermitFromSurveySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -49,18 +35,11 @@ export const unassociatePermitFromSurveySQL = (surveyId: number): SQLStatement | * @returns {SQLStatement} sql query object */ export const putNewSurveyPermitNumberSQL = (surveyId: number, permitNumber: string): SQLStatement | null => { - defaultLog.debug({ - label: 'putNewSurveyPermitNumberSQL', - message: 'params', - surveyId, - permitNumber - }); - if (!surveyId || !permitNumber) { return null; } - const sqlStatement = SQL` + return SQL` UPDATE permit SET survey_id = ${surveyId} @@ -69,15 +48,6 @@ export const putNewSurveyPermitNumberSQL = (surveyId: number, permitNumber: stri AND survey_id IS NULL; `; - - defaultLog.debug({ - label: 'putNewSurveyPermitNumberSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -94,15 +64,6 @@ export const putSurveyDetailsSQL = ( data: PutSurveyDetailsData | null, revision_count: number ): SQLStatement | null => { - defaultLog.debug({ - label: 'putSurveyDetailsSQL', - message: 'params', - projectId, - surveyId, - data, - revision_count - }); - if (!projectId || !surveyId || !data) { return null; } @@ -110,7 +71,7 @@ export const putSurveyDetailsSQL = ( const geometrySqlStatement = SQL``; if (data.geometry && data.geometry.length) { - const geometryCollectionSQL = generateGeometryCollectionSQL(data.geometry); + const geometryCollectionSQL = queries.spatial.generateGeometryCollectionSQL(data.geometry); geometrySqlStatement.append(SQL` public.geography( @@ -133,14 +94,12 @@ export const putSurveyDetailsSQL = ( UPDATE survey SET name = ${data.name}, - objectives = ${data.objectives}, start_date = ${data.start_date}, end_date = ${data.end_date}, lead_first_name = ${data.lead_first_name}, lead_last_name = ${data.lead_last_name}, location_name = ${data.location_name}, geojson = ${JSON.stringify(data.geometry)}, - common_survey_methodology_id = ${data.common_survey_methodology_id}, geography = `; @@ -155,13 +114,6 @@ export const putSurveyDetailsSQL = ( revision_count = ${revision_count}; `); - defaultLog.debug({ - label: 'putSurveyDetailsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; @@ -174,19 +126,13 @@ export const putSurveyDetailsSQL = ( * @returns {SQLStatement} sql query object */ export const putSurveyProprietorSQL = (surveyId: number, data: PutSurveyProprietorData | null): SQLStatement | null => { - defaultLog.debug({ - label: 'putSurveyProprietorSQL', - message: 'params', - surveyId, - data - }); - if (!surveyId || !data) { return null; } - const sqlStatement = SQL` - UPDATE survey_proprietor + return SQL` + UPDATE + survey_proprietor SET proprietor_type_id = ${data.prt_id}, first_nations_id = ${data.fn_id}, @@ -199,15 +145,6 @@ export const putSurveyProprietorSQL = (surveyId: number, data: PutSurveyPropriet AND survey_id = ${surveyId} `; - - defaultLog.debug({ - label: 'putSurveyProprietorSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -218,8 +155,6 @@ export const putSurveyProprietorSQL = (surveyId: number, data: PutSurveyPropriet * @returns {SQLStatement} sql query object */ export const updateSurveyPublishStatusSQL = (surveyId: number, publish: boolean): SQLStatement | null => { - defaultLog.debug({ label: 'updateSurveyPublishStatusSQL', message: 'params', surveyId, publish }); - if (!surveyId) { return null; } @@ -253,12 +188,38 @@ export const updateSurveyPublishStatusSQL = (surveyId: number, publish: boolean) survey_id as id; `); - defaultLog.debug({ - label: 'updateSurveyPublishStatusSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; + +/** + * SQL query to update a survey row. + * + * @param {number} projectId + * @param {number} surveyId + * @param {PutSurveyPurposeAndMethodologyData} data + * @returns {SQLStatement} sql query object + */ +export const putSurveyPurposeAndMethodologySQL = ( + surveyId: number, + data: PutSurveyPurposeAndMethodologyData | null, + revision_count: number +): SQLStatement | null => { + if (!surveyId || !data) { + return null; + } + + return SQL` + UPDATE + survey + SET + field_method_id = ${data.field_method_id}, + additional_details = ${data.additional_details}, + ecological_season_id = ${data.ecological_season_id}, + intended_outcome_id = ${data.intended_outcome_id}, + surveyed_all_areas = ${data.surveyed_all_areas} + WHERE + survey_id = ${surveyId} + AND + revision_count = ${revision_count}; + `; +}; diff --git a/api/src/queries/survey/survey-view-queries.test.ts b/api/src/queries/survey/survey-view-queries.test.ts index 625705b707..ee6a1bfdc4 100644 --- a/api/src/queries/survey/survey-view-queries.test.ts +++ b/api/src/queries/survey/survey-view-queries.test.ts @@ -1,74 +1,108 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { getSurveyProprietorForUpdateSQL } from './survey-view-update-queries'; -import { getSurveyForViewSQL, getSurveyIdsSQL, getAllAssignablePermitsForASurveySQL } from './survey-view-queries'; -import { getSurveyListSQL } from '../../queries/survey/survey-view-queries'; +import { getSurveySQL } from '../../queries/survey/survey-view-queries'; +import { + getAllAssignablePermitsForASurveySQL, + getSurveyAncillarySpeciesDataForViewSQL, + getSurveyBasicDataForViewSQL, + getSurveyFocalSpeciesDataForViewSQL, + getSurveyFundingSourcesDataForViewSQL, + getSurveyIdsSQL +} from './survey-view-queries'; -describe('getSurveyProprietorSQL', () => { - it('returns null when null survey id param provided', () => { - const response = getSurveyProprietorForUpdateSQL((null as unknown) as number); +describe('getAllAssignablePermitsForASurveySQL', () => { + it('returns null when null project id param provided', () => { + const response = getAllAssignablePermitsForASurveySQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getSurveyProprietorForUpdateSQL(1); + const response = getAllAssignablePermitsForASurveySQL(1); expect(response).to.not.be.null; }); }); -describe('getAllAssignablePermitsForASurveySQL', () => { +describe('getSurveyIdsSQL', () => { it('returns null when null project id param provided', () => { - const response = getAllAssignablePermitsForASurveySQL((null as unknown) as number); + const response = getSurveyIdsSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getAllAssignablePermitsForASurveySQL(1); + const response = getSurveyIdsSQL(1); expect(response).to.not.be.null; }); }); -describe('getSurveyIdsSQL', () => { - it('returns null when null project id param provided', () => { - const response = getSurveyIdsSQL((null as unknown) as number); +describe('getSurveyListSQL', () => { + it('returns a null response when null project id param provided', () => { + const response = getSurveySQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getSurveyIdsSQL(1); + const response = getSurveySQL(1); expect(response).to.not.be.null; }); }); -describe('getSurveyListSQL', () => { - it('returns a null response when null project id param provided', () => { - const response = getSurveyListSQL((null as unknown) as number); +describe('getSurveyBasicDataForViewSQL', () => { + it('returns a null response when null survey id param provided', () => { + const response = getSurveyBasicDataForViewSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns a non null response when valid params passed in', () => { + const response = getSurveyBasicDataForViewSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyFundingSourcesDataForViewSQL', () => { + it('returns a null response when null survey id param provided', () => { + const response = getSurveyFundingSourcesDataForViewSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns a non null response when valid params passed in', () => { + const response = getSurveyFundingSourcesDataForViewSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyFocalSpeciesDataForViewSQL', () => { + it('returns a null response when null survey id param provided', () => { + const response = getSurveyFocalSpeciesDataForViewSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getSurveyListSQL(1); + const response = getSurveyFocalSpeciesDataForViewSQL(1); expect(response).to.not.be.null; }); }); -describe('getSurveyForViewSQL', () => { +describe('getSurveyAncillarySpeciesDataForViewSQL', () => { it('returns a null response when null survey id param provided', () => { - const response = getSurveyForViewSQL((null as unknown) as number); + const response = getSurveyAncillarySpeciesDataForViewSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getSurveyForViewSQL(1); + const response = getSurveyAncillarySpeciesDataForViewSQL(1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/survey/survey-view-queries.ts b/api/src/queries/survey/survey-view-queries.ts index 97e0dfac0e..41a0b4cf42 100644 --- a/api/src/queries/survey/survey-view-queries.ts +++ b/api/src/queries/survey/survey-view-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/survey/survey-view-queries'); /** * SQL query to get all permits applicable for a survey @@ -13,17 +10,11 @@ const defaultLog = getLogger('queries/survey/survey-view-queries'); * @returns {SQLStatement} sql query object */ export const getAllAssignablePermitsForASurveySQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getAllAssignablePermitsForASurveySQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT number, type @@ -34,15 +25,6 @@ export const getAllAssignablePermitsForASurveySQL = (projectId: number): SQLStat AND survey_id IS NULL; `; - - defaultLog.debug({ - label: 'getAllAssignablePermitsForASurveySQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -52,17 +34,11 @@ export const getAllAssignablePermitsForASurveySQL = (projectId: number): SQLStat * @returns {SQLStatement} sql query object */ export const getSurveyIdsSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSurveyIdsSQL', - message: 'params', - projectId - }); - if (!projectId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT survey_id as id FROM @@ -70,15 +46,6 @@ export const getSurveyIdsSQL = (projectId: number): SQLStatement | null => { WHERE project_id = ${projectId}; `; - - defaultLog.debug({ - label: 'getSurveyIdsSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -87,111 +54,102 @@ export const getSurveyIdsSQL = (projectId: number): SQLStatement | null => { * @param {number} projectId * @returns {SQLStatement} sql query object */ -export const getSurveyListSQL = (projectId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSurveyListSQL', - message: 'params', - projectId - }); - - if (!projectId) { +export const getSurveySQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - s.survey_id as id, - s.name, - s.start_date, - s.end_date, - s.publish_timestamp, - CONCAT_WS(' - ', wtu.english_name, CONCAT_WS(' ', wtu.unit_name1, wtu.unit_name2, wtu.unit_name3)) as species + * FROM - wldtaxonomic_units as wtu - LEFT OUTER JOIN - study_species as ss - ON - ss.wldtaxonomic_units_id = wtu.wldtaxonomic_units_id - LEFT OUTER JOIN - survey as s - ON - s.survey_id = ss.survey_id + survey WHERE - s.project_id = ${projectId}; + survey_id = ${surveyId}; `; - - defaultLog.debug({ - label: 'getSurveyListSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; -/** - * SQL query to retrieve a survey row for viewing purposes. - * - * @param {number} surveyId - * @returns {SQLStatement} sql query object - */ -export const getSurveyForViewSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSurveyForViewSQL', - message: 'params', - surveyId - }); - +export const getSurveyBasicDataForViewSQL = (surveyId: number): SQLStatement | null => { if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT s.survey_id as id, s.name, - s.objectives, + s.additional_details, + s.field_method_id, + s.ecological_season_id, + s.intended_outcome_id, + s.surveyed_all_areas, s.start_date, s.end_date, s.lead_first_name, s.lead_last_name, s.location_name, s.geojson as geometry, - per.number, - per.type, - csm.name as common_survey_methodology, - sfs.project_funding_source_id as pfs_id, - pfs.funding_amount::numeric::int, - pfs.funding_start_date, - pfs.funding_end_date, - fs.name as agency_name, s.revision_count, s.publish_timestamp as publish_date, - os.occurrence_submission_id, - sss.survey_summary_submission_id, - CASE - WHEN ss.is_focal = TRUE - THEN CONCAT_WS(' - ', wtu.english_name, CONCAT_WS(' ', wtu.unit_name1, wtu.unit_name2, wtu.unit_name3)) - END as focal_species, - CASE - WHEN ss.is_focal = FALSE - THEN CONCAT_WS(' - ', wtu.english_name, CONCAT_WS(' ', wtu.unit_name1, wtu.unit_name2, wtu.unit_name3)) - END as ancillary_species + per.number, + per.type, + max(os.occurrence_submission_id) as occurrence_submission_id, + max(sss.survey_summary_submission_id) as survey_summary_submission_id FROM - wldtaxonomic_units as wtu + survey as s LEFT OUTER JOIN - study_species as ss + permit as per ON - ss.wldtaxonomic_units_id = wtu.wldtaxonomic_units_id + per.survey_id = s.survey_id LEFT OUTER JOIN - survey as s + field_method as fm ON - s.survey_id = ss.survey_id + fm.field_method_id = s.field_method_id LEFT OUTER JOIN - permit as per + occurrence_submission as os ON - per.survey_id = s.survey_id + os.survey_id = s.survey_id + LEFT OUTER JOIN + survey_summary_submission sss + ON + sss.survey_id = s.survey_id + WHERE + s.survey_id = ${surveyId} + GROUP BY + s.survey_id, + s.name, + s.field_method_id, + s.additional_details, + s.intended_outcome_id, + s.surveyed_all_areas, + s.ecological_season_id, + s.start_date, + s.end_date, + s.lead_first_name, + s.lead_last_name, + s.location_name, + s.geojson, + s.revision_count, + s.publish_timestamp, + per.number, + per.type; + `; +}; + +export const getSurveyFundingSourcesDataForViewSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { + return null; + } + + return SQL` + SELECT + sfs.project_funding_source_id as pfs_id, + pfs.funding_amount::numeric::int, + pfs.funding_start_date, + pfs.funding_end_date, + fs.name as agency_name + FROM + survey as s LEFT OUTER JOIN survey_funding_source as sfs ON @@ -208,31 +166,49 @@ export const getSurveyForViewSQL = (surveyId: number): SQLStatement | null => { funding_source as fs ON iac.funding_source_id = fs.funding_source_id - LEFT OUTER JOIN - common_survey_methodology as csm - ON - csm.common_survey_methodology_id = s.common_survey_methodology_id - LEFT OUTER JOIN - occurrence_submission as os - ON - os.survey_id = s.survey_id - LEFT OUTER JOIN - survey_summary_submission sss - ON - sss.survey_id = s.survey_id WHERE s.survey_id = ${surveyId} - ORDER BY - os.event_timestamp DESC - LIMIT 1; + GROUP BY + sfs.project_funding_source_id, + pfs.funding_amount::numeric::int, + pfs.funding_start_date, + pfs.funding_end_date, + fs.name + order by + pfs.funding_start_date; `; +}; - defaultLog.debug({ - label: 'getSurveyForViewSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +export const getSurveyFocalSpeciesDataForViewSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { + return null; + } - return sqlStatement; + return SQL` + SELECT + wldtaxonomic_units_id + FROM + study_species + WHERE + survey_id = ${surveyId} + AND + is_focal = TRUE; + `; +}; + +export const getSurveyAncillarySpeciesDataForViewSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { + return null; + } + + return SQL` + SELECT + wldtaxonomic_units_id + FROM + study_species + WHERE + survey_id = ${surveyId} + AND + is_focal = FALSE; + `; }; diff --git a/api/src/queries/survey/survey-view-update-queries.test.ts b/api/src/queries/survey/survey-view-update-queries.test.ts index 1f790f0dd2..afdb76e2e6 100644 --- a/api/src/queries/survey/survey-view-update-queries.test.ts +++ b/api/src/queries/survey/survey-view-update-queries.test.ts @@ -1,16 +1,30 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { getSurveyForViewSQL } from './survey-view-queries'; +import { getSurveyDetailsForUpdateSQL, getSurveyProprietorForUpdateSQL } from './survey-view-update-queries'; -describe('getSurveySQL', () => { +describe('getSurveyDetailsForUpdateSQL', () => { it('returns null when null survey id param provided', () => { - const response = getSurveyForViewSQL((null as unknown) as number); + const response = getSurveyDetailsForUpdateSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns a non null response when valid params passed in', () => { - const response = getSurveyForViewSQL(1); + const response = getSurveyDetailsForUpdateSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('getSurveyProprietorSQL', () => { + it('returns null when null survey id param provided', () => { + const response = getSurveyProprietorForUpdateSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns a non null response when valid params passed in', () => { + const response = getSurveyProprietorForUpdateSQL(1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/survey/survey-view-update-queries.ts b/api/src/queries/survey/survey-view-update-queries.ts index 742147af0a..f03e6cf714 100644 --- a/api/src/queries/survey/survey-view-update-queries.ts +++ b/api/src/queries/survey/survey-view-update-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/survey/survey-view-queries'); /** * SQL query to retrieve a survey row for update purposes. @@ -10,21 +7,15 @@ const defaultLog = getLogger('queries/survey/survey-view-queries'); * @returns {SQLStatement} sql query object */ export const getSurveyDetailsForUpdateSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSurveyDetailsForUpdateSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT s.survey_id as id, s.name, - s.objectives, + s.additional_details, s.start_date, s.end_date, s.lead_first_name, @@ -32,23 +23,37 @@ export const getSurveyDetailsForUpdateSQL = (surveyId: number): SQLStatement | n s.location_name, s.geojson as geometry, s.revision_count, - s.common_survey_methodology_id, + s.field_method_id, + s.surveyed_all_areas, + s.publish_timestamp as publish_date, per.number, per.type, - sfs.project_funding_source_id as pfs_id, - s.publish_timestamp as publish_date, - CASE - WHEN ss.is_focal = TRUE THEN wtu.wldtaxonomic_units_id - END as focal_species, - CASE - WHEN ss.is_focal = FALSE THEN wtu.wldtaxonomic_units_id - END as ancillary_species + array_remove( + array_agg( + distinct sfs.project_funding_source_id + ), + NULL + ) as pfs_id, + array_remove( + array_agg( + DISTINCT CASE + WHEN ss.is_focal = TRUE + THEN ss.wldtaxonomic_units_id + END + ), + NULL + ) as focal_species, + array_remove( + array_agg( + DISTINCT CASE + WHEN ss.is_focal = FALSE + THEN ss.wldtaxonomic_units_id + END + ), + NULL + ) as ancillary_species FROM - wldtaxonomic_units as wtu - LEFT OUTER JOIN study_species as ss - ON - ss.wldtaxonomic_units_id = wtu.wldtaxonomic_units_id LEFT OUTER JOIN survey as s ON @@ -62,17 +67,24 @@ export const getSurveyDetailsForUpdateSQL = (surveyId: number): SQLStatement | n ON sfs.survey_id = s.survey_id WHERE - s.survey_id = ${surveyId}; + s.survey_id = ${surveyId} + group by + s.survey_id, + s.name, + s.additional_details, + s.start_date, + s.end_date, + s.lead_first_name, + s.lead_last_name, + s.location_name, + s.geojson, + s.revision_count, + s.field_method_id, + s.surveyed_all_areas, + s.publish_timestamp, + per.number, + per.type; `; - - defaultLog.debug({ - label: 'getSurveyDetailsForUpdateSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -82,17 +94,11 @@ export const getSurveyDetailsForUpdateSQL = (surveyId: number): SQLStatement | n * @returns {SQLStatement} sql query object */ export const getSurveyProprietorForUpdateSQL = (surveyId: number): SQLStatement | null => { - defaultLog.debug({ - label: 'getSurveyProprietorForUpdateSQL', - message: 'params', - surveyId - }); - if (!surveyId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT sp.survey_proprietor_id as id, prt.name as proprietor_type_name, @@ -116,13 +122,57 @@ export const getSurveyProprietorForUpdateSQL = (surveyId: number): SQLStatement where survey_id = ${surveyId}; `; +}; + +/** + * SQL query to retrieve a survey_proprietor row. + * + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const getSurveyPurposeAndMethodologyForUpdateSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { + return null; + } + + return SQL` + SELECT + s.survey_id as id, + s.field_method_id, + s.additional_details, + s.ecological_season_id, + s.intended_outcome_id, + s.surveyed_all_areas, + sv.vantage_id, + s.revision_count + FROM + survey s + LEFT OUTER JOIN + survey_vantage sv + ON + sv.survey_id = s.survey_id + WHERE + s.survey_id = ${surveyId}; + `; +}; - defaultLog.debug({ - label: 'getSurveyProprietorForUpdateSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +/** + * SQL query to retrieve a survey_proprietor row. + * + * @param {number} surveyId + * @returns {SQLStatement} sql query object + */ +export const getSurveyVantageCodesSQL = (surveyId: number): SQLStatement | null => { + if (!surveyId) { + return null; + } - return sqlStatement; + return SQL` + SELECT + vantage_id + FROM + survey_vantage + WHERE + survey_id = ${surveyId}; + `; }; diff --git a/api/src/queries/user-context-queries.ts b/api/src/queries/user-context-queries.ts deleted file mode 100644 index 349aecbb75..0000000000 --- a/api/src/queries/user-context-queries.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SQL, SQLStatement } from 'sql-template-strings'; -import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; -import { getLogger } from '../utils/logger'; - -const defaultLog = getLogger('queries/user-context-queries'); - -export const setSystemUserContextSQL = ( - userIdentifier: string, - systemUserType: SYSTEM_IDENTITY_SOURCE -): SQLStatement | null => { - defaultLog.debug({ label: 'setSystemUserContextSQL', message: 'params', userIdentifier, systemUserType }); - - if (!userIdentifier) { - return null; - } - - const sqlStatement = SQL`select api_set_context(${userIdentifier}, ${systemUserType});`; - - defaultLog.debug({ - label: 'setSystemUserContextSQL', - message: 'sql', - 'sqlStatement.text': JSON.stringify(sqlStatement.text), - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; diff --git a/api/src/queries/users/index.ts b/api/src/queries/users/index.ts new file mode 100644 index 0000000000..c09ae671ea --- /dev/null +++ b/api/src/queries/users/index.ts @@ -0,0 +1,4 @@ +import * as systemRole from './system-role-queries'; +import * as user from './user-queries'; + +export default { ...systemRole, ...user }; diff --git a/api/src/queries/users/system-role-queries.test.ts b/api/src/queries/users/system-role-queries.test.ts index f341341c45..1221b57ee5 100644 --- a/api/src/queries/users/system-role-queries.test.ts +++ b/api/src/queries/users/system-role-queries.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { deleteSystemRolesSQL, postSystemRolesSQL } from './system-role-queries'; +import { postSystemRolesSQL } from './system-role-queries'; describe('postSystemRolesSQL', () => { it('returns null response when null userId provided', () => { @@ -27,29 +27,3 @@ describe('postSystemRolesSQL', () => { expect(response).to.not.be.null; }); }); - -describe('deleteSystemRolesSQL', () => { - it('returns null response when null userId provided', () => { - const response = deleteSystemRolesSQL((null as unknown) as number, [1]); - - expect(response).to.be.null; - }); - - it('returns null response when null roleIds provided', () => { - const response = deleteSystemRolesSQL(1, (null as unknown) as number[]); - - expect(response).to.be.null; - }); - - it('returns null response when empty roleIds provided', () => { - const response = deleteSystemRolesSQL(1, []); - - expect(response).to.be.null; - }); - - it('returns non null response when valid parameters provided', () => { - const response = deleteSystemRolesSQL(1, [1, 2]); - - expect(response).to.not.be.null; - }); -}); diff --git a/api/src/queries/users/system-role-queries.ts b/api/src/queries/users/system-role-queries.ts index 651e2d7f49..bf21309d7c 100644 --- a/api/src/queries/users/system-role-queries.ts +++ b/api/src/queries/users/system-role-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/user/system-role-queries'); /** * SQL query to add one or more system roles to a user. @@ -11,13 +8,12 @@ const defaultLog = getLogger('queries/user/system-role-queries'); * @return {*} {(SQLStatement | null)} */ export const postSystemRolesSQL = (userId: number, roleIds: number[]): SQLStatement | null => { - defaultLog.debug({ label: 'postSystemRolesSQL', message: 'params', userId, roleIds }); - if (!userId || !roleIds?.length) { return null; } const sqlStatement = SQL` + INSERT INTO system_user_role ( system_user_id, system_role_id @@ -35,54 +31,5 @@ export const postSystemRolesSQL = (userId: number, roleIds: number[]): SQLStatem sqlStatement.append(';'); - defaultLog.debug({ - label: 'postSystemRolesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; -}; - -/** - * SQL query to remove one or more system roles from a user. - * - * @param {number} userId - * @param {number[]} roleIds - * @return {*} {(SQLStatement | null)} - */ -export const deleteSystemRolesSQL = (userId: number, roleIds: number[]): SQLStatement | null => { - defaultLog.debug({ label: 'deleteSystemRolesSQL', message: 'params', userId, roleIds }); - - if (!userId || !roleIds?.length) { - return null; - } - - const sqlStatement = SQL` - DELETE FROM - system_user_role - WHERE - system_user_id = ${userId} - AND - system_role_id IN (`; - - // Add first element - sqlStatement.append(SQL`${roleIds[0]}`); - - for (let idx = 1; idx < roleIds.length; idx++) { - // Add subsequent elements, which get a comma prefix - sqlStatement.append(SQL`, ${roleIds[idx]}`); - } - - sqlStatement.append(SQL`);`); - - defaultLog.debug({ - label: 'deleteSystemRolesSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - return sqlStatement; }; diff --git a/api/src/queries/users/user-queries.test.ts b/api/src/queries/users/user-queries.test.ts index bf53b3858a..00693da9a4 100644 --- a/api/src/queries/users/user-queries.test.ts +++ b/api/src/queries/users/user-queries.test.ts @@ -1,6 +1,15 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { addSystemUserSQL, getUserByIdSQL, getUserByUserIdentifierSQL, getUserListSQL } from './user-queries'; +import { + activateSystemUserSQL, + addSystemUserSQL, + deactivateSystemUserSQL, + deleteAllProjectRolesSQL, + deleteAllSystemRolesSQL, + getUserByIdSQL, + getUserByUserIdentifierSQL, + getUserListSQL +} from './user-queries'; describe('getUserByUserIdentifierSQL', () => { it('returns null response when null userIdentifier provided', () => { @@ -40,31 +49,81 @@ describe('getUserListSQL', () => { describe('addSystemUserSQL', () => { it('returns null response when null userIdentifier provided', () => { - const response = addSystemUserSQL((null as unknown) as string, 'validString', 1); + const response = addSystemUserSQL((null as unknown) as string, 'validString'); expect(response).to.be.null; }); it('returns null response when null identitySource provided', () => { - const response = addSystemUserSQL('validString', (null as unknown) as string, 1); + const response = addSystemUserSQL('validString', (null as unknown) as string); expect(response).to.be.null; }); it('returns null response when null userIdentifier provided', () => { - const response = addSystemUserSQL((null as unknown) as string, 'validString', 1); + const response = addSystemUserSQL((null as unknown) as string, 'validString'); expect(response).to.be.null; }); - it('returns null response when null systemUserId provided', () => { - const response = addSystemUserSQL('validString', 'validString', (null as unknown) as number); + it('returns non null response when valid parameters provided', () => { + const response = addSystemUserSQL('validString', 'validString'); + + expect(response).to.not.be.null; + }); +}); + +describe('deactivateSystemUserSQL', () => { + it('returns null response when null userIdentifier provided', () => { + const response = deactivateSystemUserSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deactivateSystemUserSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('activateSystemUserSQL', () => { + it('returns null response when null userId provided', () => { + const response = activateSystemUserSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = activateSystemUserSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteAllSystemRolesSQL', () => { + it('returns null response when null userId provided', () => { + const response = deleteAllSystemRolesSQL((null as unknown) as number); + + expect(response).to.be.null; + }); + + it('returns non null response when valid parameters provided', () => { + const response = deleteAllSystemRolesSQL(1); + + expect(response).to.not.be.null; + }); +}); + +describe('deleteAllProjectRolesSQL', () => { + it('returns null response when null userId provided', () => { + const response = deleteAllProjectRolesSQL((null as unknown) as number); expect(response).to.be.null; }); it('returns non null response when valid parameters provided', () => { - const response = addSystemUserSQL('validString', 'validString', 1); + const response = deleteAllProjectRolesSQL(1); expect(response).to.not.be.null; }); diff --git a/api/src/queries/users/user-queries.ts b/api/src/queries/users/user-queries.ts index 932c939592..d4148d88c5 100644 --- a/api/src/queries/users/user-queries.ts +++ b/api/src/queries/users/user-queries.ts @@ -1,7 +1,4 @@ import { SQL, SQLStatement } from 'sql-template-strings'; -import { getLogger } from '../../utils/logger'; - -const defaultLog = getLogger('queries/user/user-queries'); /** * SQL query to get a single user and their system roles, based on their user_identifier. @@ -10,16 +7,15 @@ const defaultLog = getLogger('queries/user/user-queries'); * @returns {SQLStatement} sql query object */ export const getUserByUserIdentifierSQL = (userIdentifier: string): SQLStatement | null => { - defaultLog.debug({ label: 'getUserByUserIdentifierSQL', message: 'params', userIdentifier }); - if (!userIdentifier) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - su.system_user_id as id, + su.system_user_id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -36,17 +32,9 @@ export const getUserByUserIdentifierSQL = (userIdentifier: string): SQLStatement su.user_identifier = ${userIdentifier} GROUP BY su.system_user_id, + su.record_end_date, su.user_identifier; `; - - defaultLog.debug({ - label: 'getUserByUserIdentifierSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -56,16 +44,15 @@ export const getUserByUserIdentifierSQL = (userIdentifier: string): SQLStatement * @returns {SQLStatement} sql query object */ export const getUserByIdSQL = (userId: number): SQLStatement | null => { - defaultLog.debug({ label: 'getUserByIdSQL', message: 'params', userId }); - if (!userId) { return null; } - const sqlStatement = SQL` + return SQL` SELECT - su.system_user_id as id, + su.system_user_id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -80,19 +67,13 @@ export const getUserByIdSQL = (userId: number): SQLStatement | null => { sur.system_role_id = sr.system_role_id WHERE su.system_user_id = ${userId} + AND + su.record_end_date IS NULL GROUP BY su.system_user_id, + su.record_end_date, su.user_identifier; `; - - defaultLog.debug({ - label: 'getUserByIdSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -101,12 +82,11 @@ export const getUserByIdSQL = (userId: number): SQLStatement | null => { * @returns {SQLStatement} sql query object */ export const getUserListSQL = (): SQLStatement | null => { - defaultLog.debug({ label: 'getUserListSQL', message: 'getUserListSQL' }); - - const sqlStatement = SQL` + return SQL` SELECT - su.system_user_id as id, + su.system_user_id, su.user_identifier, + su.record_end_date, array_remove(array_agg(sr.system_role_id), NULL) AS role_ids, array_remove(array_agg(sr.name), NULL) AS role_names FROM @@ -119,19 +99,13 @@ export const getUserListSQL = (): SQLStatement | null => { system_role sr ON sur.system_role_id = sr.system_role_id + WHERE + su.record_end_date IS NULL GROUP BY su.system_user_id, + su.record_end_date, su.user_identifier; `; - - defaultLog.debug({ - label: 'getUserListSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); - - return sqlStatement; }; /** @@ -139,51 +113,115 @@ export const getUserListSQL = (): SQLStatement | null => { * * @param {string} userIdentifier * @param {string} identitySource - * @param {number} systemUserId * @return {*} {(SQLStatement | null)} */ -export const addSystemUserSQL = ( - userIdentifier: string, - identitySource: string, - systemUserId: number -): SQLStatement | null => { - defaultLog.debug({ - label: 'addSystemUserSQL', - message: 'addSystemUserSQL', - userIdentifier, - identitySource, - systemUserId - }); - - if (!userIdentifier || !identitySource || !systemUserId) { +export const addSystemUserSQL = (userIdentifier: string, identitySource: string): SQLStatement | null => { + if (!userIdentifier || !identitySource) { return null; } - const sqlStatement = SQL` + return SQL` INSERT INTO system_user ( user_identity_source_id, user_identifier, - record_effective_date, - create_user + record_effective_date ) VALUES ( (Select user_identity_source_id FROM user_identity_source WHERE name = ${identitySource.toUpperCase()}), ${userIdentifier}, - now(), - ${systemUserId} + now() ) RETURNING - system_user_id as id, - user_identity_source_id, - user_identifier, - record_effective_date; + *; `; +}; - defaultLog.debug({ - label: 'addSystemUserSQL', - message: 'sql', - 'sqlStatement.text': sqlStatement.text, - 'sqlStatement.values': sqlStatement.values - }); +/** + * SQL query to remove one or more system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deactivateSystemUserSQL = (userId: number): SQLStatement | null => { + if (!userId) { + return null; + } + + return SQL` + UPDATE + system_user + SET + record_end_date = now() + WHERE + system_user_id = ${userId} + RETURNING + *; + `; +}; - return sqlStatement; +/** + * SQL query to activate a system user. Does nothing is the system user is already active. + * + * @param {number} userId + * @return {*} {(SQLStatement | null)} + */ +export const activateSystemUserSQL = (userId: number): SQLStatement | null => { + if (!userId) { + return null; + } + + return SQL` + UPDATE + system_user + SET + record_end_date = NULL + WHERE + system_user_id = ${userId} + RETURNING + *; + `; +}; + +/** + * SQL query to remove all system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deleteAllSystemRolesSQL = (userId: number): SQLStatement | null => { + if (!userId) { + return null; + } + + return SQL` + DELETE FROM + system_user_role + WHERE + system_user_id = ${userId} + RETURNING + *; + `; +}; + +/** + * SQL query to remove all system roles from a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {(SQLStatement | null)} + */ +export const deleteAllProjectRolesSQL = (userId: number): SQLStatement | null => { + if (!userId) { + return null; + } + + return SQL` + DELETE FROM + project_participation + WHERE + system_user_id = ${userId} + RETURNING + *; + `; }; diff --git a/api/src/request-handlers/security/authentication.test.ts b/api/src/request-handlers/security/authentication.test.ts new file mode 100644 index 0000000000..1bcbe03e94 --- /dev/null +++ b/api/src/request-handlers/security/authentication.test.ts @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import { Request } from 'express'; +import { describe } from 'mocha'; +import { HTTP401 } from '../../errors/custom-error'; +import * as authentication from './authentication'; + +describe('authenticateRequest', function () { + it('throws HTTP401 when authorization headers were null or missing', async function () { + try { + await authentication.authenticateRequest((undefined as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + + try { + await authentication.authenticateRequest(({} as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + + try { + await authentication.authenticateRequest(({ + headers: {} + } as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + }); + + it('throws HTTP401 when authorization header contains an invalid bearer token', async function () { + try { + await authentication.authenticateRequest(({ + headers: { + authorization: 'Not a bearer token' + } + } as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + + try { + await authentication.authenticateRequest(({ + headers: { + authorization: 'Bearer ' + } + } as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + + try { + await authentication.authenticateRequest(({ + headers: { + authorization: 'Bearer not-encoded' + } + } as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + + try { + await authentication.authenticateRequest(({ + headers: { + // sample encoded json web token from jwt.io (without kid header) + authorization: + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' + } + } as unknown) as Request); + expect.fail(); + } catch (actualError) { + expect(actualError).instanceOf(HTTP401); + } + }); +}); diff --git a/api/src/request-handlers/security/authentication.ts b/api/src/request-handlers/security/authentication.ts new file mode 100644 index 0000000000..ca49d85fe0 --- /dev/null +++ b/api/src/request-handlers/security/authentication.ts @@ -0,0 +1,131 @@ +import { Request } from 'express'; +import { decode, GetPublicKeyOrSecret, Secret, verify, VerifyErrors } from 'jsonwebtoken'; +import { JwksClient } from 'jwks-rsa'; +import { HTTP401 } from '../../errors/custom-error'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('request-handlers/security/authentication'); + +const KEYCLOAK_URL = + process.env.KEYCLOAK_URL || 'https://dev.oidc.gov.bc.ca/auth/realms/35r1iman/protocol/openid-connect/certs'; + +/** + * Authenticate the request by validating the authorization bearer token (JWT). + * + * Assign the bearer token to `req.keycloak_token`. + * + * @param {*} req + * @return {*} {Promise} true if the token is authenticated + * @throws {HTTP401} if the token is not authenticated + */ +export const authenticateRequest = async function (req: Request): Promise { + try { + if (!req?.headers?.authorization) { + defaultLog.warn({ label: 'authenticate', message: 'authorization headers were null or missing' }); + throw new HTTP401('Access Denied'); + } + + // Authorization header should be a string with format: Bearer xxxxxx.yyyyyyy.zzzzzz + const authorizationHeaderString = req.headers.authorization; + + // Check if the header is a valid bearer format + if (authorizationHeaderString.indexOf('Bearer ') !== 0) { + defaultLog.warn({ label: 'authenticate', message: 'authorization header did not have a bearer' }); + throw new HTTP401('Access Denied'); + } + + // Parse out token portion of the authorization header + const tokenString = authorizationHeaderString.split(' ')[1]; + + if (!tokenString) { + defaultLog.warn({ label: 'authenticate', message: 'token string was null' }); + throw new HTTP401('Access Denied'); + } + + // Decode token without verifying signature + const decodedToken = decode(tokenString, { complete: true, json: true }); + + if (!decodedToken) { + defaultLog.warn({ label: 'authenticate', message: 'decoded token was null' }); + throw new HTTP401('Access Denied'); + } + + // Get token header kid (key id) + const kid = decodedToken.header && decodedToken.header.kid; + + if (!kid) { + defaultLog.warn({ label: 'authenticate', message: 'decoded token header kid was null' }); + throw new HTTP401('Access Denied'); + } + + const jwksClient = new JwksClient({ jwksUri: KEYCLOAK_URL }); + + // Get signing key from certificate issuer + const key = await jwksClient.getSigningKey(kid); + + if (!key) { + defaultLog.warn({ label: 'authenticate', message: 'signing key was null' }); + throw new HTTP401('Access Denied'); + } + + // Parse out public portion of signing key + const signingKey = key.getPublicKey(); + + // Verify token using public signing key + const verifiedToken = verifyToken(tokenString, signingKey); + + if (!verifiedToken) { + throw new HTTP401('Access Denied'); + } + + // Add the verified token to the request for future use, if needed + req['keycloak_token'] = verifiedToken; + + return true; + } catch (error) { + defaultLog.warn({ label: 'authenticate', message: `unexpected error - ${(error as Error).message}`, error }); + throw new HTTP401('Access Denied'); + } +}; + +/** + * Verify jwt token. + * + * @param {string} tokenString + * @param {(Secret | GetPublicKeyOrSecret)} secretOrPublicKey + * @return {*} The decoded token, or null. + */ +const verifyToken = function (tokenString: string, secretOrPublicKey: Secret | GetPublicKeyOrSecret): any { + return verify(tokenString, secretOrPublicKey, verifyTokenCallback); +}; + +/** + * Callback that returns the decoded token, or null. + * + * @param {(VerifyErrors | null)} verificationError + * @param {(object | undefined)} verifiedToken + * @return {*} {(object | null | undefined)} + */ +const verifyTokenCallback = function ( + verificationError: VerifyErrors | null, + verifiedToken: object | undefined +): object | null | undefined { + if (verificationError) { + defaultLog.warn({ label: 'verifyToken', message: 'jwt verification error', verificationError }); + return null; + } + + // Verify that the token came from the expected issuer + // Example: when running in prod, only accept tokens from `sso.pathfinder...` and not `sso-dev.pathfinder...`, etc + if (!KEYCLOAK_URL.includes(verifiedToken?.['iss'])) { + defaultLog.warn({ + label: 'verifyToken', + message: 'jwt verification error: issuer mismatch', + 'actual token issuer': verifiedToken?.['iss'], + 'expected to be a substring of': KEYCLOAK_URL + }); + return null; + } + + return verifiedToken; +}; diff --git a/api/src/request-handlers/security/authorization.test.ts b/api/src/request-handlers/security/authorization.test.ts new file mode 100644 index 0000000000..ea0ad4a25b --- /dev/null +++ b/api/src/request-handlers/security/authorization.test.ts @@ -0,0 +1,787 @@ +import chai, { expect } from 'chai'; +import { Request } from 'express'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; +import * as db from '../../database/db'; +import { HTTPError } from '../../errors/custom-error'; +import { ProjectUserObject, UserObject } from '../../models/user'; +import project_participation_queries from '../../queries/project-participation'; +import { UserService } from '../../services/user-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; +import * as authorization from './authorization'; + +chai.use(sinonChai); + +describe('authorizeRequestHandler', function () { + afterEach(() => { + sinon.restore(); + }); + + it('throws a 403 error if the user is not authorized', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + sinon.stub(authorization, 'authorizeRequest').resolves(false); + + const mockAuthorizationSchemeCallback = () => { + return { or: [] }; + }; + + const requestHandler = authorization.authorizeRequestHandler(mockAuthorizationSchemeCallback); + + try { + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (error) { + expect((error as HTTPError).message).to.equal('Access Denied'); + expect((error as HTTPError).status).to.equal(403); + } + + expect(mockNext).not.to.have.been.called; + }); + + it('calls next if the user is authorized', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + sinon.stub(authorization, 'authorizeRequest').resolves(true); + + const mockAuthorizationSchemeCallback = () => { + return { or: [] }; + }; + + const requestHandler = authorization.authorizeRequestHandler(mockAuthorizationSchemeCallback); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockNext).to.have.been.calledOnce; + }); +}); + +describe('authorizeRequest', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns false if systemUserObject is null', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserObject = (undefined as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockSystemUserObject); + + const mockReq = ({ authorization_scheme: {} } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(false); + }); + + it('returns true if the user is a system administrator', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserObject = ({ role_names: [] } as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockSystemUserObject); + + sinon.stub(authorization, 'authorizeSystemAdministrator').resolves(true); + + const mockReq = ({ authorization_scheme: {} } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(true); + }); + + it('returns true if the authorization_scheme is undefined', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserObject = ({ role_names: [] } as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockSystemUserObject); + + sinon.stub(authorization, 'authorizeSystemAdministrator').resolves(false); + + const mockReq = ({ authorization_scheme: undefined } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(true); + }); + + it('returns true if the user is authorized against the authorization_scheme', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserObject = ({ role_names: [] } as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockSystemUserObject); + + sinon.stub(authorization, 'authorizeSystemAdministrator').resolves(false); + + sinon.stub(authorization, 'executeAuthorizationScheme').resolves(true); + + const mockReq = ({ authorization_scheme: {} } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(true); + }); + + it('returns false if the user is not authorized against the authorization_scheme', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserObject = ({ role_names: [] } as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockSystemUserObject); + + sinon.stub(authorization, 'authorizeSystemAdministrator').resolves(false); + + sinon.stub(authorization, 'executeAuthorizationScheme').resolves(false); + + const mockReq = ({ authorization_scheme: {} } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(false); + }); + + it('returns false if an error is thrown', async function () { + const mockDBConnection = getMockDBConnection({ + open: () => { + throw new Error('Test Error'); + } + }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockReq = ({ authorization_scheme: {} } as unknown) as Request; + const isAuthorized = await authorization.authorizeRequest(mockReq); + + expect(isAuthorized).to.equal(false); + }); +}); + +describe('executeAuthorizationScheme', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns false if any AND authorizationScheme rules return false', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizationScheme = ({ and: [] } as unknown) as authorization.AuthorizationScheme; + const mockDBConnection = getMockDBConnection(); + + sinon.stub(authorization, 'executeAuthorizeConfig').resolves([true, false, true]); + + const isAuthorized = await authorization.executeAuthorizationScheme( + mockReq, + mockAuthorizationScheme, + mockDBConnection + ); + + expect(isAuthorized).to.equal(false); + }); + + it('returns true if all AND authorizationScheme rules return true', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizationScheme = ({ and: [] } as unknown) as authorization.AuthorizationScheme; + const mockDBConnection = getMockDBConnection(); + + sinon.stub(authorization, 'executeAuthorizeConfig').resolves([true, true, true]); + + const isAuthorized = await authorization.executeAuthorizationScheme( + mockReq, + mockAuthorizationScheme, + mockDBConnection + ); + + expect(isAuthorized).to.equal(true); + }); + + it('returns false if all OR authorizationScheme rules return false', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizationScheme = ({ or: [] } as unknown) as authorization.AuthorizationScheme; + const mockDBConnection = getMockDBConnection(); + + sinon.stub(authorization, 'executeAuthorizeConfig').resolves([false, false, false]); + + const isAuthorized = await authorization.executeAuthorizationScheme( + mockReq, + mockAuthorizationScheme, + mockDBConnection + ); + + expect(isAuthorized).to.equal(false); + }); + + it('returns true if any OR authorizationScheme rules return true', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizationScheme = ({ or: [] } as unknown) as authorization.AuthorizationScheme; + const mockDBConnection = getMockDBConnection(); + + sinon.stub(authorization, 'executeAuthorizeConfig').resolves([false, true, false]); + + const isAuthorized = await authorization.executeAuthorizationScheme( + mockReq, + mockAuthorizationScheme, + mockDBConnection + ); + + expect(isAuthorized).to.equal(true); + }); +}); + +describe('executeAuthorizeConfig', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns an array of authorizeRule results', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeRules: authorization.AuthorizeRule[] = [ + { + validSystemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }, + { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: 1, + discriminator: 'ProjectRole' + }, + { + discriminator: 'SystemUser' + } + ]; + const mockDBConnection = getMockDBConnection(); + + sinon.stub(authorization, 'authorizeBySystemRole').resolves(true); + sinon.stub(authorization, 'authorizeByProjectRole').resolves(false); + sinon.stub(authorization, 'authorizeBySystemUser').resolves(true); + + const authorizeResults = await authorization.executeAuthorizeConfig(mockReq, mockAuthorizeRules, mockDBConnection); + + expect(authorizeResults).to.eql([true, false, true]); + }); +}); + +describe('authorizeBySystemRole', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns false if `authorizeSystemRoles` is null', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeSystemRoles = (null as unknown) as authorization.AuthorizeBySystemRoles; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemRole( + mockReq, + mockAuthorizeSystemRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns false if `systemUserObject` is null', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeSystemRoles: authorization.AuthorizeBySystemRoles = { + validSystemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }; + const mockDBConnection = getMockDBConnection(); + + const mockGetSystemUsersObjectResponse = (null as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockGetSystemUsersObjectResponse); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemRole( + mockReq, + mockAuthorizeSystemRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns true if `authorizeSystemRoles` specifies no valid roles', async function () { + const mockReq = ({ system_user: {} } as unknown) as Request; + const mockAuthorizeSystemRoles: authorization.AuthorizeBySystemRoles = { + validSystemRoles: [], + discriminator: 'SystemRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemRole( + mockReq, + mockAuthorizeSystemRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(true); + }); + + it('returns false if the user does not have any valid roles', async function () { + const mockReq = ({ system_user: { role_names: [] } } as unknown) as Request; + const mockAuthorizeSystemRoles: authorization.AuthorizeBySystemRoles = { + validSystemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemRole( + mockReq, + mockAuthorizeSystemRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns true if the user has at least one of the valid roles', async function () { + const mockReq = ({ system_user: { role_names: [SYSTEM_ROLE.PROJECT_CREATOR] } } as unknown) as Request; + const mockAuthorizeSystemRoles: authorization.AuthorizeBySystemRoles = { + validSystemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], + discriminator: 'SystemRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemRole( + mockReq, + mockAuthorizeSystemRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(true); + }); +}); + +describe('authorizeByProjectRole', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns false if `authorizeByProjectRole` is null', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeProjectRoles = (null as unknown) as authorization.AuthorizeByProjectRoles; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns false if `authorizeProjectRoles.projectId` is null', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeProjectRoles: authorization.AuthorizeByProjectRoles = { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: (null as unknown) as number, + discriminator: 'ProjectRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns true if `authorizeByProjectRole` specifies no valid roles', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeProjectRoles: authorization.AuthorizeByProjectRoles = { + validProjectRoles: [], + projectId: 1, + discriminator: 'ProjectRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(true); + }); + + it('returns false if it fails to fetch the users project role information', async function () { + const mockReq = ({} as unknown) as Request; + const mockAuthorizeProjectRoles: authorization.AuthorizeByProjectRoles = { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: 1, + discriminator: 'ProjectRole' + }; + const mockDBConnection = getMockDBConnection(); + + const mockProjectUserObject = (undefined as unknown) as ProjectUserObject; + sinon.stub(authorization, 'getProjectUserObject').resolves(mockProjectUserObject); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns false if the user does not have any valid roles', async function () { + const mockProjectUserObject = ({ project_role_names: [] } as unknown) as ProjectUserObject; + const mockReq = ({ project_user: mockProjectUserObject } as unknown) as Request; + const mockAuthorizeProjectRoles: authorization.AuthorizeByProjectRoles = { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: 1, + discriminator: 'ProjectRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns true if the user has at lest one of the valid roles', async function () { + const mockProjectUserObject = ({ project_role_names: [PROJECT_ROLE.PROJECT_LEAD] } as unknown) as ProjectUserObject; + const mockReq = ({ project_user: mockProjectUserObject } as unknown) as Request; + const mockAuthorizeProjectRoles: authorization.AuthorizeByProjectRoles = { + validProjectRoles: [PROJECT_ROLE.PROJECT_LEAD], + projectId: 1, + discriminator: 'ProjectRole' + }; + const mockDBConnection = getMockDBConnection(); + + const isAuthorizedBySystemRole = await authorization.authorizeByProjectRole( + mockReq, + mockAuthorizeProjectRoles, + mockDBConnection + ); + + expect(isAuthorizedBySystemRole).to.equal(true); + }); +}); + +describe('authorizeBySystemUser', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns false if `systemUserObject` is null', async function () { + const mockReq = ({} as unknown) as Request; + const mockDBConnection = getMockDBConnection(); + + const mockGetSystemUsersObjectResponse = (null as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockGetSystemUsersObjectResponse); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemUser(mockReq, mockDBConnection); + + expect(isAuthorizedBySystemRole).to.equal(false); + }); + + it('returns true if `systemUserObject` is not null', async function () { + const mockReq = ({ system_user: {} } as unknown) as Request; + const mockDBConnection = getMockDBConnection(); + + const mockGetSystemUsersObjectResponse = (null as unknown) as UserObject; + sinon.stub(authorization, 'getSystemUserObject').resolves(mockGetSystemUsersObjectResponse); + + const isAuthorizedBySystemRole = await authorization.authorizeBySystemUser(mockReq, mockDBConnection); + + expect(isAuthorizedBySystemRole).to.equal(true); + }); +}); + +describe('userHasValidRole', () => { + describe('validSystemRoles is a string', () => { + describe('userSystemRoles is a string', () => { + it('returns true if the valid roles is empty', () => { + const response = authorization.userHasValidRole('', ''); + + expect(response).to.be.true; + }); + + it('returns false if the user has no roles', () => { + const response = authorization.userHasValidRole('admin', ''); + + expect(response).to.be.false; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole('admin', 'user'); + + expect(response).to.be.false; + }); + + it('returns true if the user has a matching role', () => { + const response = authorization.userHasValidRole('admin', 'admin'); + + expect(response).to.be.true; + }); + }); + + describe('userSystemRoles is an array', () => { + it('returns true if the valid roles is empty', () => { + const response = authorization.userHasValidRole('', []); + + expect(response).to.be.true; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole('admin', []); + + expect(response).to.be.false; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole('admin', ['user']); + + expect(response).to.be.false; + }); + + it('returns true if the user has a matching role', () => { + const response = authorization.userHasValidRole('admin', ['admin']); + + expect(response).to.be.true; + }); + }); + }); + + describe('validSystemRoles is an array', () => { + describe('userSystemRoles is a string', () => { + it('returns true if the valid roles is empty', () => { + const response = authorization.userHasValidRole([], ''); + + expect(response).to.be.true; + }); + + it('returns false if the user has no roles', () => { + const response = authorization.userHasValidRole(['admin'], ''); + + expect(response).to.be.false; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole(['admin'], 'user'); + + expect(response).to.be.false; + }); + + it('returns true if the user has a matching role', () => { + const response = authorization.userHasValidRole(['admin'], 'admin'); + + expect(response).to.be.true; + }); + }); + + describe('userSystemRoles is an array', () => { + it('returns true if the valid roles is empty', () => { + const response = authorization.userHasValidRole([], []); + + expect(response).to.be.true; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole(['admin'], []); + + expect(response).to.be.false; + }); + + it('returns false if the user has no matching roles', () => { + const response = authorization.userHasValidRole(['admin'], ['user']); + + expect(response).to.be.false; + }); + + it('returns true if the user has a matching role', () => { + const response = authorization.userHasValidRole(['admin'], ['admin']); + + expect(response).to.be.true; + }); + }); + }); +}); + +describe('getSystemUserObject', function () { + afterEach(() => { + sinon.restore(); + }); + + it('throws an HTTP500 error if fetching the system user throws an error', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + sinon.stub(authorization, 'getSystemUserWithRoles').callsFake(() => { + throw new Error('Test Error'); + }); + + try { + await authorization.getSystemUserObject(mockDBConnection); + expect.fail(); + } catch (error) { + expect((error as HTTPError).message).to.equal('failed to get system user'); + expect((error as HTTPError).status).to.equal(500); + } + }); + + it('throws an HTTP500 error if the system user is null', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserWithRolesResponse = null; + sinon.stub(authorization, 'getSystemUserWithRoles').resolves(mockSystemUserWithRolesResponse); + + try { + await authorization.getSystemUserObject(mockDBConnection); + expect.fail(); + } catch (error) { + expect((error as HTTPError).message).to.equal('system user was null'); + expect((error as HTTPError).status).to.equal(500); + } + }); + + it('returns a `UserObject`', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserWithRolesResponse = new UserObject(); + sinon.stub(authorization, 'getSystemUserWithRoles').resolves(mockSystemUserWithRolesResponse); + + const systemUserObject = await authorization.getSystemUserObject(mockDBConnection); + + expect(systemUserObject).to.equal(mockSystemUserWithRolesResponse); + }); +}); + +describe('getSystemUserWithRoles', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns null if the system user id is null', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => null }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const result = await authorization.getSystemUserWithRoles(mockDBConnection); + + expect(result).to.be.null; + }); + + it('returns a UserObject', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockUsersByIdSQLResponse = new UserObject(); + sinon.stub(UserService.prototype, 'getUserById').resolves(mockUsersByIdSQLResponse); + + const result = await authorization.getSystemUserWithRoles(mockDBConnection); + + expect(result).to.equal(mockUsersByIdSQLResponse); + }); +}); + +describe('getProjectUserObject', function () { + afterEach(() => { + sinon.restore(); + }); + + it('throws an HTTP500 error if fetching the system user throws an error', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + sinon.stub(authorization, 'getProjectUserWithRoles').callsFake(() => { + throw new Error('Test Error'); + }); + + try { + await authorization.getProjectUserObject(1, mockDBConnection); + expect.fail(); + } catch (error) { + expect((error as HTTPError).message).to.equal('failed to get project user'); + expect((error as HTTPError).status).to.equal(500); + } + }); + + it('throws an HTTP500 error if the system user is null', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserWithRolesResponse = null; + sinon.stub(authorization, 'getProjectUserWithRoles').resolves(mockSystemUserWithRolesResponse); + + try { + await authorization.getProjectUserObject(1, mockDBConnection); + expect.fail(); + } catch (error) { + expect((error as HTTPError).message).to.equal('project user was null'); + expect((error as HTTPError).status).to.equal(500); + } + }); + + it('returns a `ProjectUserObject`', async function () { + const mockDBConnection = getMockDBConnection(); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockSystemUserWithRolesResponse = {}; + sinon.stub(authorization, 'getProjectUserWithRoles').resolves(mockSystemUserWithRolesResponse); + + const systemUserObject = await authorization.getProjectUserObject(1, mockDBConnection); + + expect(systemUserObject).to.be.instanceOf(ProjectUserObject); + }); +}); + +describe('getProjectUserWithRoles', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns null if the system user id is null', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => null }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const result = await authorization.getProjectUserWithRoles(1, mockDBConnection); + + expect(result).to.be.null; + }); + + it('returns null if the get user by id SQL statement is null', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockUsersByIdSQLResponse = null; + sinon + .stub(project_participation_queries, 'getProjectParticipationBySystemUserSQL') + .returns(mockUsersByIdSQLResponse); + + const result = await authorization.getProjectUserWithRoles(1, mockDBConnection); + + expect(result).to.be.null; + }); + + it('returns the first row of the response', async function () { + const mockResponseRow = { 'Test Column': 'Test Value' }; + const mockQueryResponse = ({ rowCount: 1, rows: [mockResponseRow] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + sinon.stub(db, 'getDBConnection').returns(mockDBConnection); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon + .stub(project_participation_queries, 'getProjectParticipationBySystemUserSQL') + .returns(mockUsersByIdSQLResponse); + + const result = await authorization.getProjectUserWithRoles(1, mockDBConnection); + + expect(result).to.eql(mockResponseRow); + }); +}); diff --git a/api/src/request-handlers/security/authorization.ts b/api/src/request-handlers/security/authorization.ts new file mode 100644 index 0000000000..f0f2193c58 --- /dev/null +++ b/api/src/request-handlers/security/authorization.ts @@ -0,0 +1,390 @@ +import { Request } from 'express'; +import { RequestHandler } from 'express-serve-static-core'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../../constants/roles'; +import { getDBConnection, IDBConnection } from '../../database/db'; +import { HTTP403, HTTP500 } from '../../errors/custom-error'; +import { ProjectUserObject, UserObject } from '../../models/user'; +import { queries } from '../../queries/queries'; +import { UserService } from '../../services/user-service'; +import { getLogger } from '../../utils/logger'; + +const defaultLog = getLogger('request-handlers/security/authorization'); + +export enum AuthorizeOperator { + AND = 'and', + OR = 'or' +} + +export interface AuthorizeBySystemRoles { + validSystemRoles: SYSTEM_ROLE[]; + discriminator: 'SystemRole'; +} + +export interface AuthorizeByProjectRoles { + validProjectRoles: PROJECT_ROLE[]; + projectId: number; + discriminator: 'ProjectRole'; +} + +export interface AuthorizeBySystemUser { + discriminator: 'SystemUser'; +} + +export type AuthorizeRule = AuthorizeBySystemRoles | AuthorizeByProjectRoles | AuthorizeBySystemUser; + +export type AuthorizeConfigOr = { + [AuthorizeOperator.AND]?: never; + [AuthorizeOperator.OR]: AuthorizeRule[]; +}; + +export type AuthorizeConfigAnd = { + [AuthorizeOperator.AND]: AuthorizeRule[]; + [AuthorizeOperator.OR]?: never; +}; + +export type AuthorizationScheme = AuthorizeConfigAnd | AuthorizeConfigOr; + +export type AuthorizationSchemeCallback = (req: Request) => AuthorizationScheme; + +/** + * Authorize a user against the `AuthorizationScheme` returned by `authorizationSchemeCallback`. + * + * Calls `next()` if the user is authorized. + * + * @export + * @param {AuthorizationSchemeCallback} authorizationSchemeCallback + * @throws {HTTP403} Access Denied if the user is not authorized. + * @return {*} {RequestHandler} + */ +export function authorizeRequestHandler(authorizationSchemeCallback: AuthorizationSchemeCallback): RequestHandler { + return async (req, res, next) => { + req['authorization_scheme'] = authorizationSchemeCallback(req); + + const isAuthorized = await authorizeRequest(req); + + if (!isAuthorized) { + defaultLog.warn({ label: 'authorize', message: 'User is not authorized' }); + throw new HTTP403('Access Denied'); + } + + // User is authorized + next(); + }; +} + +/** + * Returns `true` if the user is authorized successfully against the `AuthorizationScheme` in + * `req['authorization_scheme']`, `false` otherwise. + * + * Note: System administrators are automatically granted access, regardless of the authorization scheme provided. + * + * @param {Request} req + * @return {*} {Promise} + */ +export const authorizeRequest = async (req: Request): Promise => { + const connection = getDBConnection(req['keycloak_token']); + + try { + const authorizationScheme: AuthorizationScheme = req['authorization_scheme']; + + if (!authorizationScheme) { + // No authorization scheme specified, all authenticated users are authorized + return true; + } + + await connection.open(); + + const isAuthorized = + (await authorizeSystemAdministrator(req, connection)) || + (await executeAuthorizationScheme(req, authorizationScheme, connection)); + + await connection.commit(); + + return isAuthorized; + } catch (error) { + defaultLog.error({ label: 'authorize', message: 'error', error }); + await connection.rollback(); + return false; + } finally { + connection.release(); + } +}; + +/** + * Execute the `authorizationScheme` against the current user, and return `true` if they have access, `false` otherwise. + * + * @param {Request} req + * @param {UserObject} systemUserObject + * @param {AuthorizationScheme} authorizationScheme + * @param {IDBConnection} connection + * @return {*} {Promise} `true` if the `authorizationScheme` indicates the user has access, `false` otherwise. + */ +export const executeAuthorizationScheme = async ( + req: Request, + authorizationScheme: AuthorizationScheme, + connection: IDBConnection +): Promise => { + if (authorizationScheme.and) { + return (await executeAuthorizeConfig(req, authorizationScheme.and, connection)).every((item) => item); + } else { + return (await executeAuthorizeConfig(req, authorizationScheme.or, connection)).some((item) => item); + } +}; + +/** + * Execute an array of `AuthorizeRule`, returning an array of boolean results. + * + * @param {Request} req + * @param {AuthorizeRule[]} authorizeRules + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const executeAuthorizeConfig = async ( + req: Request, + authorizeRules: AuthorizeRule[], + connection: IDBConnection +): Promise => { + const authorizeResults: boolean[] = []; + + for (const authorizeRule of authorizeRules) { + switch (authorizeRule.discriminator) { + case 'SystemRole': + authorizeResults.push(await authorizeBySystemRole(req, authorizeRule, connection)); + break; + case 'ProjectRole': + authorizeResults.push(await authorizeByProjectRole(req, authorizeRule, connection)); + break; + case 'SystemUser': + authorizeResults.push(await authorizeBySystemUser(req, connection)); + break; + } + } + + return authorizeResults; +}; + +/** + * Check if the user has the system administrator role. + * + * @param {UserObject} systemUserObject + * @return {*} {boolean} `true` if the user is a system administrator, `false` otherwise. + */ +export const authorizeSystemAdministrator = async (req: Request, connection: IDBConnection): Promise => { + const systemUserObject: UserObject = req['system_user'] || (await getSystemUserObject(connection)); + + // Add the system_user to the request for future use, if needed + req['system_user'] = systemUserObject; + + if (!systemUserObject) { + // Cannot verify user roles + return false; + } + + return systemUserObject.role_names.includes(SYSTEM_ROLE.SYSTEM_ADMIN); +}; + +/** + * Check that the user has at least one of the valid system roles specified in `authorizeSystemRoles.validSystemRoles`. + * + * @param {UserObject} systemUserObject + * @param {AuthorizeBySystemRoles} authorizeSystemRoles + * @return {*} {boolean} `true` if the user has at least one valid system role role, or no valid system roles are + * specified; `false` otherwise. + */ +export const authorizeBySystemRole = async ( + req: Request, + authorizeSystemRoles: AuthorizeBySystemRoles, + connection: IDBConnection +): Promise => { + if (!authorizeSystemRoles) { + // Cannot verify user roles + return false; + } + + const systemUserObject: UserObject = req['system_user'] || (await getSystemUserObject(connection)); + + // Add the system_user to the request for future use, if needed + req['system_user'] = systemUserObject; + + if (!systemUserObject) { + // Cannot verify user roles + return false; + } + + if (systemUserObject.record_end_date) { + //system user has an expired record + return false; + } + + // Check if the user has at least 1 of the valid roles + return userHasValidRole(authorizeSystemRoles.validSystemRoles, systemUserObject?.role_names); +}; + +/** + * Check that the user has at least on of the valid project roles specified in `authorizeProjectRoles.validProjectRoles`. + * + * @param {Request} req + * @param {AuthorizeByProjectRoles} authorizeProjectRoles + * @param {IDBConnection} connection + * @return {*} {Promise} `Promise` if the user has at least one valid project role, or no valid project + * roles are specified; `Promise` otherwise. + */ +export const authorizeByProjectRole = async ( + req: Request, + authorizeProjectRoles: AuthorizeByProjectRoles, + connection: IDBConnection +): Promise => { + if (!authorizeProjectRoles || !authorizeProjectRoles.projectId) { + // No project id to verify roles for + return false; + } + + if (!authorizeProjectRoles?.validProjectRoles.length) { + // No valid rules specified + return true; + } + + const projectUserObject: ProjectUserObject = + req['project_user'] || (await getProjectUserObject(authorizeProjectRoles.projectId, connection)); + + // Add the project_user to the request for future use, if needed + req['project_user'] = projectUserObject; + + if (!projectUserObject) { + defaultLog.warn({ label: 'getProjectUser', message: 'project user was null' }); + return false; + } + + return userHasValidRole(authorizeProjectRoles.validProjectRoles, projectUserObject.project_role_names); +}; + +/** + * Check if the user is a valid system user. + * + * @param {Request} req + * @param {IDBConnection} connection + * @return {*} {Promise} `Promise` if the user is a valid system user, `Promise` otherwise. + */ +export const authorizeBySystemUser = async (req: Request, connection: IDBConnection): Promise => { + const systemUserObject: UserObject = req['system_user'] || (await getSystemUserObject(connection)); + + // Add the system_user to the request for future use, if needed + req['system_user'] = systemUserObject; + + if (!systemUserObject) { + // Cannot verify user roles + return false; + } + + // User is a valid system user + return true; +}; + +/** + * Compares an array of user roles against an array of valid roles. + * + * @param {(string | string[])} validRoles valid roles to match against + * @param {(string | string[])} userRoles user roles to check against the valid roles + * @return {*} {boolean} true if the user has at least 1 of the valid roles or no valid roles are specified, false + * otherwise + */ +export const userHasValidRole = function (validRoles: string | string[], userRoles: string | string[]): boolean { + if (!validRoles || !validRoles.length) { + return true; + } + + if (!Array.isArray(validRoles)) { + validRoles = [validRoles]; + } + + if (!Array.isArray(userRoles)) { + userRoles = [userRoles]; + } + + for (const validRole of validRoles) { + if (userRoles.includes(validRole)) { + return true; + } + } + + return false; +}; + +export const getSystemUserObject = async (connection: IDBConnection): Promise => { + let systemUserWithRoles; + + try { + systemUserWithRoles = await getSystemUserWithRoles(connection); + } catch { + throw new HTTP500('failed to get system user'); + } + + if (!systemUserWithRoles) { + throw new HTTP500('system user was null'); + } + + return systemUserWithRoles; +}; + +/** + * Finds a single user based on their keycloak token information. + * + * @param {IDBConnection} connection + * @return {*} {(Promise)} + * @return {*} + */ +export const getSystemUserWithRoles = async (connection: IDBConnection): Promise => { + const systemUserId = connection.systemUserId(); + + if (!systemUserId) { + return null; + } + + const userService = new UserService(connection); + + return userService.getUserById(systemUserId); +}; + +export const getProjectUserObject = async ( + projectId: number, + connection: IDBConnection +): Promise => { + let projectUserWithRoles; + + try { + projectUserWithRoles = await getProjectUserWithRoles(projectId, connection); + } catch { + throw new HTTP500('failed to get project user'); + } + + if (!projectUserWithRoles) { + throw new HTTP500('project user was null'); + } + + return new ProjectUserObject(projectUserWithRoles); +}; + +/** + * Get a user's project roles, for a single project. + * + * @param {number} projectId + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getProjectUserWithRoles = async function (projectId: number, connection: IDBConnection): Promise { + const systemUserId = connection.systemUserId(); + + if (!systemUserId || !projectId) { + return null; + } + + const sqlStatement = queries.projectParticipation.getProjectParticipationBySystemUserSQL(projectId, systemUserId); + + if (!sqlStatement) { + return null; + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + return response.rows[0] || null; +}; diff --git a/api/src/security/auth-utils.test.ts b/api/src/security/auth-utils.test.ts deleted file mode 100644 index 4dfa4c6e5e..0000000000 --- a/api/src/security/auth-utils.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import chai, { expect } from 'chai'; -import { describe } from 'mocha'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import * as auth_utils from './auth-utils'; -import * as db from '../database/db'; -import * as user_queries from '../queries/users/user-queries'; -import { QueryResult } from 'pg'; -import SQL from 'sql-template-strings'; -import { HTTP401, HTTP403 } from '../errors/CustomError'; -import { getMockDBConnection } from '../__mocks__/db'; - -chai.use(sinonChai); - -describe('userHasValidSystemRoles', () => { - describe('validSystemRoles is a string', () => { - describe('userSystemRoles is a string', () => { - it('returns true if the valid roles is empty', () => { - const response = auth_utils.userHasValidSystemRoles('', ''); - - expect(response).to.be.true; - }); - - it('returns false if the user has no roles', () => { - const response = auth_utils.userHasValidSystemRoles('admin', ''); - - expect(response).to.be.false; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles('admin', 'user'); - - expect(response).to.be.false; - }); - - it('returns true if the user has a matching role', () => { - const response = auth_utils.userHasValidSystemRoles('admin', 'admin'); - - expect(response).to.be.true; - }); - }); - - describe('userSystemRoles is an array', () => { - it('returns true if the valid roles is empty', () => { - const response = auth_utils.userHasValidSystemRoles('', []); - - expect(response).to.be.true; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles('admin', []); - - expect(response).to.be.false; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles('admin', ['user']); - - expect(response).to.be.false; - }); - - it('returns true if the user has a matching role', () => { - const response = auth_utils.userHasValidSystemRoles('admin', ['admin']); - - expect(response).to.be.true; - }); - }); - }); - - describe('validSystemRoles is an array', () => { - describe('userSystemRoles is a string', () => { - it('returns true if the valid roles is empty', () => { - const response = auth_utils.userHasValidSystemRoles([], ''); - - expect(response).to.be.true; - }); - - it('returns false if the user has no roles', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], ''); - - expect(response).to.be.false; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], 'user'); - - expect(response).to.be.false; - }); - - it('returns true if the user has a matching role', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], 'admin'); - - expect(response).to.be.true; - }); - }); - - describe('userSystemRoles is an array', () => { - it('returns true if the valid roles is empty', () => { - const response = auth_utils.userHasValidSystemRoles([], []); - - expect(response).to.be.true; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], []); - - expect(response).to.be.false; - }); - - it('returns false if the user has no matching roles', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], ['user']); - - expect(response).to.be.false; - }); - - it('returns true if the user has a matching role', () => { - const response = auth_utils.userHasValidSystemRoles(['admin'], ['admin']); - - expect(response).to.be.true; - }); - }); - }); -}); - -describe('getSystemUser', function () { - afterEach(() => { - sinon.restore(); - }); - - const keycloakToken = { - key: 'value' - }; - - const dbConnectionObj = getMockDBConnection(); - - it('should return null when no system user id', async function () { - sinon.stub(db, 'getDBConnection').returns(dbConnectionObj); - - const result = await auth_utils.getSystemUser(keycloakToken); - - expect(result).to.be.null; - }); - - it('should return null when getUserByIdSql fails', async function () { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(null); - - const result = await auth_utils.getSystemUser(keycloakToken); - - expect(result).to.be.null; - }); - - it('should return the user row on success', async function () { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return { - rowCount: 1, - rows: [ - { - id: 1, - user_identifier: 'identifier', - role_ids: [1, 2], - role_names: ['role 1', 'role 2'] - } - ] - } as QueryResult; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = await auth_utils.getSystemUser(keycloakToken); - - expect(result.id).to.equal(1); - expect(result.user_identifier).to.equal('identifier'); - expect(result.role_ids).to.eql([1, 2]); - expect(result.role_names).to.eql(['role 1', 'role 2']); - }); - - it('should return null when response has no rowCount (no user found)', async function () { - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - return 20; - }, - query: async () => { - return ({ - rowCount: 0, - rows: [] - } as unknown) as QueryResult; - } - }); - - sinon.stub(user_queries, 'getUserByIdSQL').returns(SQL`some query`); - - const result = await auth_utils.getSystemUser(keycloakToken); - - expect(result).to.be.null; - }); - - it('should throw an error when a failure occurs', async function () { - const expectedError = new Error('cannot process query'); - - sinon.stub(db, 'getDBConnection').returns({ - ...dbConnectionObj, - systemUserId: () => { - throw expectedError; - } - }); - - try { - await auth_utils.getSystemUser(keycloakToken); - expect.fail(); - } catch (actualError) { - expect(actualError.message).to.equal(expectedError.message); - } - }); -}); - -describe('authorize', function () { - afterEach(() => { - sinon.restore(); - }); - - it('throws HTTP403 when the keycloak_token is empty', async function () { - try { - await auth_utils.authorize({ keycloak_token: '' }, ['abc']); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP403); - } - }); - - it('throws HTTP403 when the keycloak_token is undefined', async function () { - try { - await auth_utils.authorize(undefined, ['abc']); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP403); - } - }); - - it('returns true without scopes', async function () { - const result = await auth_utils.authorize({ keycloak_token: 'some token' }, []); - expect(result).to.be.true; - }); - - it('throws HTTP403 when stubbed getSystemUser returns null', async function () { - sinon.stub(auth_utils, 'getSystemUser').resolves(null); - - try { - await auth_utils.authorize({ keycloak_token: 'some token' }, ['abc']); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP403); - } - }); - - it('throws HTTP403 when stubbed getSystemUser throws error', async function () { - sinon.stub(auth_utils, 'getSystemUser').rejects(new Error()); - try { - await auth_utils.authorize({ keycloak_token: 'any token' }, ['abc']); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP403); - expect(actualError.message).to.equal('Access Denied'); - } - }); - - it('throws HTTP403 when userHasValidSystemRoles returns falsie', async function () { - sinon.stub(auth_utils, 'getSystemUser').resolves({ - id: 0, - user_identifier: 'somebody', - role_ids: [], - role_names: [] - }); - sinon.stub(auth_utils, 'userHasValidSystemRoles').returns(false); - - try { - await auth_utils.authorize({ keycloak_token: 'any token' }, ['abc']); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP403); - } - }); - - it('authorizes a user with valid roles', async function () { - sinon.stub(auth_utils, 'getSystemUser').resolves({ - id: 0, - user_identifier: 'somebody', - role_ids: [], - role_names: ['Role 1'] - }); - - const result = await auth_utils.authorize({ keycloak_token: 'any token' }, ['Role 1']); - expect(result).to.be.true; - }); -}); - -describe('authenticate', function () { - it('throws HTTP401 when authorization headers were null or missing', async function () { - try { - await auth_utils.authenticate(undefined); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - - try { - await auth_utils.authenticate({}); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - - try { - await auth_utils.authenticate({ - headers: {} - }); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - }); - - it('throws HTTP401 when authorization header contains an invalid bearer token', async function () { - try { - await auth_utils.authenticate({ - headers: { - authorization: 'Not a bearer token' - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - - try { - await auth_utils.authenticate({ - headers: { - authorization: 'Bearer ' - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - - try { - await auth_utils.authenticate({ - headers: { - authorization: 'Bearer not-encoded' - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - - try { - await auth_utils.authenticate({ - headers: { - // sample encoded json web token from jwt.io (without kid header) - authorization: - 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' - } - }); - expect.fail(); - } catch (actualError) { - expect(actualError).instanceOf(HTTP401); - } - }); -}); diff --git a/api/src/security/auth-utils.ts b/api/src/security/auth-utils.ts deleted file mode 100644 index 2855eb320f..0000000000 --- a/api/src/security/auth-utils.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { decode, GetPublicKeyOrSecret, Secret, verify, VerifyErrors } from 'jsonwebtoken'; -import JwksRsa, { JwksClient } from 'jwks-rsa'; -import { promisify } from 'util'; -import { getDBConnection } from '../database/db'; -import { HTTP401, HTTP403 } from '../errors/CustomError'; -import { UserObject } from '../models/user'; -import { getUserByIdSQL } from '../queries/users/user-queries'; -import { getLogger } from '../utils/logger'; - -const defaultLog = getLogger('security/auth-utils'); - -const KEYCLOAK_URL = - process.env.KEYCLOAK_URL || 'https://dev.oidc.gov.bc.ca/auth/realms/35r1iman/protocol/openid-connect/certs'; - -/** - * Authenticate the request by validating the authorization bearer token. - * - * @param {*} req - * @return {*} {Promise} true if the token is authenticated - * @throws {HTTP401} if the token is not authenticated - */ -export const authenticate = async function (req: any): Promise { - try { - if (!req?.headers?.authorization) { - defaultLog.warn({ label: 'authenticate', message: 'authorization headers were null or missing' }); - throw new HTTP401('Access Denied'); - } - - // Authorization header should be a string with format: Bearer xxxxxx.yyyyyyy.zzzzzz - const authorizationHeaderString = req.headers.authorization; - - // Check if the header is a valid bearer format - if (authorizationHeaderString.indexOf('Bearer ') !== 0) { - defaultLog.warn({ label: 'authenticate', message: 'authorization header did not have a bearer' }); - throw new HTTP401('Access Denied'); - } - - // Parse out token portion of the authorization header - const tokenString = authorizationHeaderString.split(' ')[1]; - - if (!tokenString) { - defaultLog.warn({ label: 'authenticate', message: 'token string was null' }); - throw new HTTP401('Access Denied'); - } - - // Decode token without verifying signature - const decodedToken = decode(tokenString, { complete: true, json: true }); - - if (!decodedToken) { - defaultLog.warn({ label: 'authenticate', message: 'decoded token was null' }); - throw new HTTP401('Access Denied'); - } - - // Get token header kid (key id) - const kid = decodedToken.header && decodedToken.header.kid; - - if (!kid) { - defaultLog.warn({ label: 'authenticate', message: 'decoded token header kid was null' }); - throw new HTTP401('Access Denied'); - } - - const jwksClient: JwksClient = JwksRsa({ jwksUri: KEYCLOAK_URL }); - - const getSigningKeyAsync = promisify(jwksClient.getSigningKey); - - // Get signing key from certificate issuer - const key = await getSigningKeyAsync(kid); - - if (!key) { - defaultLog.warn({ label: 'authenticate', message: 'signing key was null' }); - throw new HTTP401('Access Denied'); - } - - // Parse out public portion of signing key - const signingKey = key['publicKey'] || key['rsaPublicKey']; - - // Verify token using public signing key - const verifiedToken = verifyToken(tokenString, signingKey); - - if (!verifiedToken) { - throw new HTTP401('Access Denied'); - } - - // Add the verified token to the request for future use, if needed - req.keycloak_token = verifiedToken; - - return true; - } catch (error) { - defaultLog.warn({ label: 'authenticate', message: `unexpected error - ${error.message}`, error }); - throw new HTTP401('Access Denied'); - } -}; - -/** - * Verify jwt token. - * - * @param {string} tokenString - * @param {(Secret | GetPublicKeyOrSecret)} secretOrPublicKey - * @return {*} The decoded token, or null. - */ -const verifyToken = function (tokenString: string, secretOrPublicKey: Secret | GetPublicKeyOrSecret): any { - return verify(tokenString, secretOrPublicKey, verifyTokenCallback); -}; - -/** - * Callback that returns the decoded token, or null. - * - * @param {(VerifyErrors | null)} verificationError - * @param {(object | undefined)} verifiedToken - * @return {*} {(object | null | undefined)} - */ -const verifyTokenCallback = function ( - verificationError: VerifyErrors | null, - verifiedToken: object | undefined -): object | null | undefined { - if (verificationError) { - defaultLog.warn({ label: 'verifyToken', message: 'jwt verification error', verificationError }); - return null; - } - - // Verify that the token came from the expected issuer - // Example: when running in prod, only accept tokens from `sso.pathfinder...` and not `sso-dev.pathfinder...`, etc - if (!KEYCLOAK_URL.includes(verifiedToken?.['iss'])) { - defaultLog.warn({ - label: 'verifyToken', - message: 'jwt verification error: issuer mismatch', - 'actual token issuer': verifiedToken?.['iss'], - 'expected to be a substring of': KEYCLOAK_URL - }); - return null; - } - - return verifiedToken; -}; - -/** - * Authenticate the current user against the current route by validating their roles against the route scopes (roles). - * - * @param {*} req - * @param {string[]} scopes identifiers (typically roles) that the user roles/rules/etc must be valid against - * @returns {*} {Promise} true if the user is authorized - * @throws {HTTP403} if the user is not authorized - */ -export const authorize = async function (req: any, scopes: string[]): Promise { - if (!req?.keycloak_token) { - defaultLog.warn({ label: 'authorize', message: 'request is missing a keycloak token' }); - throw new HTTP403('Access Denied'); - } - - if (!scopes || !scopes.length) { - return true; - } - - let systemUserWithRoles; - - try { - systemUserWithRoles = await getSystemUser(req.keycloak_token); - } catch { - defaultLog.warn({ label: 'authorize', message: 'failed to get system user' }); - throw new HTTP403('Access Denied'); - } - - if (!systemUserWithRoles) { - defaultLog.warn({ label: 'authorize', message: 'failed to get system user' }); - throw new HTTP403('Access Denied'); - } - - const userObject = new UserObject(systemUserWithRoles); - - const hasValidSystemRole = userHasValidSystemRoles(scopes, userObject.role_names); - - if (!hasValidSystemRole) { - defaultLog.warn({ label: 'authorize', message: 'system user does not have any valid system roles' }); - throw new HTTP403('Access Denied'); - } - - req.system_user = userObject; - - return true; -}; - -/** - * Finds a single user based on their keycloak token information. - * - * @param {object} keycloakToken - * @return {*} - */ -export const getSystemUser = async function (keycloakToken: object) { - const connection = getDBConnection(keycloakToken); - - try { - await connection.open(); - - const systemUserId = connection.systemUserId(); - - if (!systemUserId) { - return null; - } - - const sqlStatement = getUserByIdSQL(systemUserId); - - if (!sqlStatement) { - return null; - } - - const response = await connection.query(sqlStatement.text, sqlStatement.values); - - await connection.commit(); - - return (response && response.rowCount && response.rows[0]) || null; - } catch (error) { - defaultLog.error({ label: 'getSystemUser', message: 'error', error }); - throw error; - } finally { - connection.release(); - } -}; - -/** - * Checks a set of user system roles against a set of valid system roles. - * - * @param {(string | string[])} validSystemRoles one or more valid roles to match against - * @param {(string | string[])} userSystemRoles one or more user roles to check against the valid roles - * @return {boolean} true if the user has at least 1 of the valid roles, false otherwise - */ -export const userHasValidSystemRoles = function ( - validSystemRoles: string | string[], - userSystemRoles: string | string[] -): boolean { - if (!validSystemRoles || !validSystemRoles.length) { - return true; - } - - if (!Array.isArray(validSystemRoles)) { - validSystemRoles = [validSystemRoles]; - } - - if (!Array.isArray(userSystemRoles)) { - userSystemRoles = [userSystemRoles]; - } - - for (const validRole of validSystemRoles) { - if (userSystemRoles.includes(validRole)) { - return true; - } - } - - return false; -}; diff --git a/api/src/services/code-service.test.ts b/api/src/services/code-service.test.ts new file mode 100644 index 0000000000..d6c5972178 --- /dev/null +++ b/api/src/services/code-service.test.ts @@ -0,0 +1,52 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMockDBConnection } from '../__mocks__/db'; +import { CodeService } from './code-service'; + +chai.use(sinonChai); + +describe('CodeService', () => { + describe('getAllCodeSets', function () { + afterEach(() => { + sinon.restore(); + }); + + it('returns all code sets', async function () { + const mockQuery = sinon.stub(); + mockQuery.resolves({ + rows: [{ id: 1, name: 'codeName' }] + }); + + const mockDBConnection = getMockDBConnection({ query: mockQuery }); + + const codeService = new CodeService(mockDBConnection); + + const response = await codeService.getAllCodeSets(); + + expect(response).to.have.all.keys( + 'management_action_type', + 'first_nations', + 'funding_source', + 'investment_action_category', + 'activity', + 'project_type', + 'coordinator_agency', + 'region', + 'proprietor_type', + 'iucn_conservation_action_level_1_classification', + 'iucn_conservation_action_level_2_subclassification', + 'iucn_conservation_action_level_3_subclassification', + 'system_roles', + 'project_roles', + 'regional_offices', + 'administrative_activity_status_type', + 'ecological_seasons', + 'field_methods', + 'intended_outcomes', + 'vantage_codes' + ); + }); + }); +}); diff --git a/api/src/services/code-service.ts b/api/src/services/code-service.ts new file mode 100644 index 0000000000..a58385a1ef --- /dev/null +++ b/api/src/services/code-service.ts @@ -0,0 +1,128 @@ +import { coordinator_agency, region, regional_offices } from '../constants/codes'; +import { queries } from '../queries/queries'; +import { getLogger } from '../utils/logger'; +import { DBService } from './service'; + +const defaultLog = getLogger('queries/code-queries'); + +/** + * A single code value. + * + * @export + * @interface ICode + */ +export interface ICode { + id: number; + name: string; +} + +/** + * A code set (an array of ICode values). + */ +export type CodeSet = T[]; + +export interface IAllCodeSets { + management_action_type: CodeSet; + first_nations: CodeSet; + funding_source: CodeSet; + investment_action_category: CodeSet<{ id: number; fs_id: number; name: string }>; + activity: CodeSet; + project_type: CodeSet; + coordinator_agency: CodeSet; + region: CodeSet; + proprietor_type: CodeSet<{ id: number; name: string; is_first_nation: boolean }>; + iucn_conservation_action_level_1_classification: CodeSet; + iucn_conservation_action_level_2_subclassification: CodeSet<{ id: number; iucn1_id: number; name: string }>; + iucn_conservation_action_level_3_subclassification: CodeSet<{ id: number; iucn2_id: number; name: string }>; + system_roles: CodeSet; + project_roles: CodeSet; + regional_offices: CodeSet; + administrative_activity_status_type: CodeSet; + field_methods: CodeSet<{ id: number; name: string; description: string }>; + ecological_seasons: CodeSet<{ id: number; name: string; description: string }>; + intended_outcomes: CodeSet<{ id: number; name: string; description: string }>; + vantage_codes: CodeSet; +} + +export class CodeService extends DBService { + /** + * Function that fetches all code sets. + * + * @return {*} {Promise} an object containing all code sets + * @memberof CodeService + */ + async getAllCodeSets(): Promise { + defaultLog.debug({ message: 'getAllCodeSets' }); + + const [ + management_action_type, + first_nations, + funding_source, + investment_action_category, + activity, + iucn_conservation_action_level_1_classification, + iucn_conservation_action_level_2_subclassification, + iucn_conservation_action_level_3_subclassification, + proprietor_type, + project_type, + system_roles, + project_roles, + administrative_activity_status_type, + field_methods, + ecological_seasons, + intended_outcomes, + vantage_codes + ] = await Promise.all([ + await this.connection.query(queries.codes.getManagementActionTypeSQL().text), + await this.connection.query(queries.codes.getFirstNationsSQL().text), + await this.connection.query(queries.codes.getFundingSourceSQL().text), + await this.connection.query(queries.codes.getInvestmentActionCategorySQL().text), + await this.connection.query(queries.codes.getActivitySQL().text), + await this.connection.query(queries.codes.getIUCNConservationActionLevel1ClassificationSQL().text), + await this.connection.query(queries.codes.getIUCNConservationActionLevel2SubclassificationSQL().text), + await this.connection.query(queries.codes.getIUCNConservationActionLevel3SubclassificationSQL().text), + await this.connection.query(queries.codes.getProprietorTypeSQL().text), + await this.connection.query(queries.codes.getProjectTypeSQL().text), + await this.connection.query(queries.codes.getSystemRolesSQL().text), + await this.connection.query(queries.codes.getProjectRolesSQL().text), + await this.connection.query(queries.codes.getAdministrativeActivityStatusTypeSQL().text), + await this.connection.query(queries.codes.getFieldMethodsSQL().text), + await this.connection.query(queries.codes.getEcologicalSeasonsSQL().text), + await this.connection.query(queries.codes.getIntendedOutcomesSQL().text), + await this.connection.query(queries.codes.getVantageCodesSQL().text) + ]); + + return { + management_action_type: (management_action_type && management_action_type.rows) || [], + first_nations: (first_nations && first_nations.rows) || [], + funding_source: (funding_source && funding_source.rows) || [], + investment_action_category: (investment_action_category && investment_action_category.rows) || [], + activity: (activity && activity.rows) || [], + iucn_conservation_action_level_1_classification: + (iucn_conservation_action_level_1_classification && iucn_conservation_action_level_1_classification.rows) || [], + iucn_conservation_action_level_2_subclassification: + (iucn_conservation_action_level_2_subclassification && + iucn_conservation_action_level_2_subclassification.rows) || + [], + iucn_conservation_action_level_3_subclassification: + (iucn_conservation_action_level_3_subclassification && + iucn_conservation_action_level_3_subclassification.rows) || + [], + proprietor_type: (proprietor_type && proprietor_type.rows) || [], + project_type: (project_type && project_type.rows) || [], + system_roles: (system_roles && system_roles.rows) || [], + project_roles: (project_roles && project_roles.rows) || [], + administrative_activity_status_type: + (administrative_activity_status_type && administrative_activity_status_type.rows) || [], + field_methods: (field_methods && field_methods.rows) || [], + ecological_seasons: (ecological_seasons && ecological_seasons.rows) || [], + intended_outcomes: (intended_outcomes && intended_outcomes.rows) || [], + vantage_codes: (vantage_codes && vantage_codes.rows) || [], + + // TODO Temporarily hard coded list of code values below + coordinator_agency, + region, + regional_offices + }; + } +} diff --git a/api/src/services/gcnotify-service.test.ts b/api/src/services/gcnotify-service.test.ts new file mode 100644 index 0000000000..cc035361df --- /dev/null +++ b/api/src/services/gcnotify-service.test.ts @@ -0,0 +1,116 @@ +import axios from 'axios'; +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiError } from '../errors/custom-error'; +import { IgcNotifyGenericMessage } from '../models/gcnotify'; +import { GCNotifyService } from './gcnotify-service'; + +chai.use(sinonChai); + +describe('GCNotifyService', () => { + describe('sendEmailGCNotification', () => { + afterEach(() => { + sinon.restore(); + }); + + const emailAddress = 'test@email.com'; + + const message = { + subject: 'message.subject', + header: 'message.header', + body1: 'message.body1', + body2: 'message.body2', + footer: 'message.footer' + }; + + it('should throw a 400 error when no email is given', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: null }); + + try { + await gcNotifyServiece.sendEmailGCNotification('', message); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to send Notification'); + } + }); + + it('should throw a 400 error when no data is given', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: null }); + + try { + await gcNotifyServiece.sendEmailGCNotification(emailAddress, message); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to send Notification'); + } + }); + + it('should not throw an error on success', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: 201 }); + + const result = await gcNotifyServiece.sendEmailGCNotification(emailAddress, {} as IgcNotifyGenericMessage); + + expect(result).to.eql(201); + }); + }); + + describe('sendPhoneNumberGCNotification', () => { + afterEach(() => { + sinon.restore(); + }); + + const sms = '2501231234'; + + const message = { + subject: 'message.subject', + header: 'message.header', + body1: 'message.body1', + body2: 'message.body2', + footer: 'message.footer' + }; + + it('should throw a 400 error when no phone number is given', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: null }); + + try { + await gcNotifyServiece.sendPhoneNumberGCNotification('', message); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to send Notification'); + } + }); + + it('should throw a 400 error when no data is given', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: null }); + + try { + await gcNotifyServiece.sendPhoneNumberGCNotification(sms, message); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to send Notification'); + } + }); + + it('should not throw an error on success', async () => { + const gcNotifyServiece = new GCNotifyService(); + + sinon.stub(axios, 'post').resolves({ data: 201 }); + + const result = await gcNotifyServiece.sendPhoneNumberGCNotification(sms, {} as IgcNotifyGenericMessage); + + expect(result).to.eql(201); + }); + }); +}); diff --git a/api/src/services/gcnotify-service.ts b/api/src/services/gcnotify-service.ts new file mode 100644 index 0000000000..6ec439d21b --- /dev/null +++ b/api/src/services/gcnotify-service.ts @@ -0,0 +1,80 @@ +import axios from 'axios'; +import { ApiError, ApiErrorType } from '../errors/custom-error'; +import { IgcNotifyGenericMessage, IgcNotifyPostReturn } from '../models/gcnotify'; + +const EMAIL_TEMPLATE = process.env.GCNOTIFY_ONBOARDING_REQUEST_EMAIL_TEMPLATE || ''; +const SMS_TEMPLATE = process.env.GCNOTIFY_ONBOARDING_REQUEST_SMS_TEMPLATE || ''; +const EMAIL_URL = process.env.GCNOTIFY_EMAIL_URL || ''; +const SMS_URL = process.env.GCNOTIFY_SMS_URL || ''; +const API_KEY = process.env.GCNOTIFY_SECRET_API_KEY || ''; + +const config = { + headers: { + Authorization: API_KEY, + 'Content-Type': 'application/json' + } +}; +export class GCNotifyService { + /** + * Send email notification to recipient + * + * + * @param {string} emailAddress + * @param {IgcNotifyGenericMessage} message + * @returns {IgcNotifyPostReturn} + */ + async sendEmailGCNotification(emailAddress: string, message: IgcNotifyGenericMessage): Promise { + const data = { + email_address: emailAddress, + template_id: EMAIL_TEMPLATE, + personalisation: { + subject: message.subject, + header: message.header, + main_body1: message.body1, + main_body2: message.body2, + footer: message.footer + } + }; + + const response = await axios.post(EMAIL_URL, data, config); + + const result = (response && response.data) || null; + + if (!result) { + throw new ApiError(ApiErrorType.UNKNOWN, 'Failed to send Notification'); + } + + return result; + } + + /** + * Send email notification to recipient + * + * + * @param {string} sms + * @param {IgcNotifyGenericMessage} message + * @returns {IgcNotifyPostReturn} + */ + async sendPhoneNumberGCNotification(sms: string, message: IgcNotifyGenericMessage): Promise { + const data = { + phone_number: sms, + template_id: SMS_TEMPLATE, + personalisation: { + header: message.header, + main_body1: message.body1, + main_body2: message.body2, + footer: message.footer + } + }; + + const response = await axios.post(SMS_URL, data, config); + + const result = (response && response.data) || null; + + if (!result) { + throw new ApiError(ApiErrorType.UNKNOWN, 'Failed to send Notification'); + } + + return result; + } +} diff --git a/api/src/services/keycloak-service.test.ts b/api/src/services/keycloak-service.test.ts new file mode 100644 index 0000000000..8d3a9f0822 --- /dev/null +++ b/api/src/services/keycloak-service.test.ts @@ -0,0 +1,158 @@ +import axios from 'axios'; +import chai, { expect } from 'chai'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiGeneralError } from '../errors/custom-error'; +import { KeycloakService } from './keycloak-service'; + +chai.use(sinonChai); + +describe('KeycloakService', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getKeycloakToken', async () => { + it('authenticates with keycloak and returns an access token', async () => { + process.env.KEYCLOAK_HOST = 'host'; + process.env.KEYCLOAK_REALM = 'realm'; + process.env.KEYCLOAK_ADMIN_USERNAME = 'admin'; + process.env.KEYCLOAK_ADMIN_PASSWORD = 'password'; + + const mockAxiosResponse = { data: { access_token: 'token' } }; + + const axiosStub = sinon.stub(axios, 'post').resolves(mockAxiosResponse); + + const keycloakService = new KeycloakService(); + + const response = await keycloakService.getKeycloakToken(); + + expect(response).to.eql('token'); + + expect(axiosStub).to.have.been.calledWith( + 'host/auth/realms/realm/protocol/openid-connect/token', + 'grant_type=client_credentials', + { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + auth: { username: 'admin', password: 'password' } + } + ); + }); + + it('catches and re-throws an error', async () => { + sinon.stub(axios, 'post').rejects(new Error('a test error')); + + const keycloakService = new KeycloakService(); + + try { + await keycloakService.getKeycloakToken(); + + expect.fail(); + } catch (error) { + expect((error as ApiGeneralError).message).to.equal('Failed to authenticate with keycloak'); + expect((error as ApiGeneralError).errors).to.eql(['a test error']); + } + }); + }); + + describe('getUserByUsername', async () => { + it('authenticates with keycloak and returns an access token', async () => { + sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + + const mockAxiosResponse = { + data: [ + { + id: 123, + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + enabled: true, + username: 'username', + attributes: { + idir_user_guid: ['string1'], + idir_userid: ['string2'], + idir_guid: ['string3'], + displayName: ['string4'] + } + } + ] + }; + + const axiosStub = sinon.stub(axios, 'get').resolves(mockAxiosResponse); + + const keycloakService = new KeycloakService(); + + const response = await keycloakService.getUserByUsername('test@idir'); + + expect(response).to.eql({ + id: 123, + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + enabled: true, + username: 'username', + attributes: { + idir_user_guid: ['string1'], + idir_userid: ['string2'], + idir_guid: ['string3'], + displayName: ['string4'] + } + }); + + expect(axiosStub).to.have.been.calledWith('host/auth/admin/realms/realm/users/?username=test%40idir', { + headers: { authorization: 'Bearer token' } + }); + }); + + it('throws an error if no users are found', async () => { + sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + + sinon.stub(axios, 'get').resolves({ data: [] }); + + const keycloakService = new KeycloakService(); + + try { + await keycloakService.getUserByUsername('test@idir'); + + expect.fail(); + } catch (error) { + expect((error as ApiGeneralError).message).to.equal('Failed to get user info from keycloak'); + expect((error as ApiGeneralError).errors).to.eql(['Found no matching keycloak users']); + } + }); + + it('throws an error if more than 1 user is found', async () => { + sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + + sinon.stub(axios, 'get').resolves({ data: [{}, {}, {}] }); + + const keycloakService = new KeycloakService(); + + try { + await keycloakService.getUserByUsername('test@idir'); + + expect.fail(); + } catch (error) { + expect((error as ApiGeneralError).message).to.equal('Failed to get user info from keycloak'); + expect((error as ApiGeneralError).errors).to.eql(['Found too many matching keycloak users']); + } + }); + + it('catches and re-throws an error', async () => { + sinon.stub(KeycloakService.prototype, 'getKeycloakToken').resolves('token'); + + sinon.stub(axios, 'get').rejects(new Error('a test error')); + + const keycloakService = new KeycloakService(); + + try { + await keycloakService.getUserByUsername('test@idir'); + + expect.fail(); + } catch (error) { + expect((error as ApiGeneralError).message).to.equal('Failed to get user info from keycloak'); + expect((error as ApiGeneralError).errors).to.eql(['a test error']); + } + }); + }); +}); diff --git a/api/src/services/keycloak-service.ts b/api/src/services/keycloak-service.ts new file mode 100644 index 0000000000..b0945a82ee --- /dev/null +++ b/api/src/services/keycloak-service.ts @@ -0,0 +1,137 @@ +import axios from 'axios'; +import qs from 'qs'; +import { ApiGeneralError } from '../errors/custom-error'; + +type KeycloakUserData = { + id: string; + createdTimestamp: number; + username: string; + enabled: boolean; + totp: boolean; + emailVerified: boolean; + firstName: string; + lastName: string; + email: string; + attributes: IDIRAttributes | BCEIDAttributes; + disableableCredentialTypes: []; + requiredActions: []; + notBefore: number; + access: { + manageGroupMembership: boolean; + view: boolean; + mapRoles: boolean; + impersonate: boolean; + manage: boolean; + }; +}; + +type IDIRAttributes = { + idir_user_guid: [string]; + idir_userid: [string]; + idir_guid: [string]; + displayName: [string]; +}; + +type BCEIDAttributes = { + bceid_userid: [string]; + displayName: [string]; +}; + +export type KeycloakUser = { + id: string; + username: string; + firstName: string; + lastName: string; + email: string; + enabled: boolean; + attributes: IDIRAttributes | BCEIDAttributes; +}; + +/** + * Service for calling the keycloak admin API. + * + * @export + * @class KeycloakService + */ +export class KeycloakService { + keycloakRealmUrl: string; + keycloakAdminUrl: string; + + constructor() { + this.keycloakRealmUrl = `${process.env.KEYCLOAK_HOST}/auth/realms/${process.env.KEYCLOAK_REALM}`; + this.keycloakAdminUrl = `${process.env.KEYCLOAK_HOST}/auth/admin/realms/${process.env.KEYCLOAK_REALM}`; + } + + /** + * Get an access token from keycloak for the service account user. + * + * @return {*} {Promise} + * @memberof KeycloakService + */ + async getKeycloakToken(): Promise { + try { + const { data } = await axios.post( + `${this.keycloakRealmUrl}/protocol/openid-connect/token`, + qs.stringify({ grant_type: 'client_credentials' }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + auth: { + username: process.env.KEYCLOAK_ADMIN_USERNAME as string, + password: process.env.KEYCLOAK_ADMIN_PASSWORD as string + } + } + ); + + return data.access_token as string; + } catch (error) { + throw new ApiGeneralError('Failed to authenticate with keycloak', [(error as Error).message]); + } + } + + /** + * Fetch keycloak user data by the keycloak username. + * + * Note on IDIR and BCEID usernames: + * - Format is `@idir` or `@bceid` + * + * @param {string} username + * @return {*} {Promise} + * @memberof KeycloakService + */ + async getUserByUsername(username: string): Promise { + const token = await this.getKeycloakToken(); + + try { + const { data } = await axios.get( + `${this.keycloakAdminUrl}/users/?${qs.stringify({ username: username })}`, + { + headers: { + authorization: `Bearer ${token}` + } + } + ); + + if (!data.length) { + throw new ApiGeneralError('Found no matching keycloak users'); + } + + if (data.length !== 1) { + throw new ApiGeneralError('Found too many matching keycloak users'); + } + + return { + id: data[0].id, + firstName: data[0].firstName, + lastName: data[0].lastName, + email: data[0].email, + enabled: data[0].enabled, + username: data[0].username, + attributes: data[0].attributes + }; + } catch (error) { + throw new ApiGeneralError('Failed to get user info from keycloak', [(error as Error).message]); + } + } +} diff --git a/api/src/services/permit-service.test.ts b/api/src/services/permit-service.test.ts new file mode 100644 index 0000000000..5319b43d09 --- /dev/null +++ b/api/src/services/permit-service.test.ts @@ -0,0 +1,313 @@ +import chai, { expect } from 'chai'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import { HTTPError } from '../errors/custom-error'; +import permit_queries from '../queries/permit'; +import { getMockDBConnection } from '../__mocks__/db'; +import { PermitService } from './permit-service'; + +chai.use(sinonChai); + +describe('PermitService', () => { + describe('getAllPermits', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for permits', async () => { + const mockDBConnection = getMockDBConnection(); + const systemUserId = 22; + + sinon.stub(permit_queries, 'getAllPermitsSQL').returns(null); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.getAllPermits(systemUserId); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + } + }); + + it('should return null when permits response has no rows', async () => { + const mockQueryResponse = (null as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const systemUserId = 22; + + sinon.stub(permit_queries, 'getAllPermitsSQL').returns(SQL`some query`); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.getAllPermits(systemUserId); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get all user permits'); + } + }); + + it('should return all permits on success', async () => { + const allPermits = [ + { + id: 1, + number: '123', + type: 'scientific', + coordinator_agency: 'agency', + project_name: 'project 1' + }, + { + id: 2, + number: '12345', + type: 'wildlife', + coordinator_agency: 'agency 2', + project_name: null + } + ]; + + const mockQueryResponse = ({ rows: allPermits } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const systemUserId = 22; + + sinon.stub(permit_queries, 'getAllPermitsSQL').returns(SQL`some query`); + + const permitService = new PermitService(mockDBConnection); + const result = await permitService.getAllPermits(systemUserId); + + expect(result).to.eql(allPermits); + }); + }); + + describe('getNonSamplingPermits', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement returned for non-sampling permits', async () => { + const mockDBConnection = getMockDBConnection(); + const systemUserId = 22; + + sinon.stub(permit_queries, 'getNonSamplingPermitsSQL').returns(null); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.getNonSamplingPermits(systemUserId); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + } + }); + + it('should throw a 400 error when permits response has no rows', async () => { + const mockQueryResponse = (null as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const systemUserId = 22; + + sinon.stub(permit_queries, 'getNonSamplingPermitsSQL').returns(SQL`some query`); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.getNonSamplingPermits(systemUserId); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to get all user permits'); + } + }); + + it('should return non-sampling permits on success', async () => { + const nonSamplingPermits = [ + { + permit_id: 1, + number: '123', + type: 'scientific' + }, + { + permit_id: 2, + number: '12345', + type: 'wildlife' + } + ]; + + const mockQueryResponse = ({ rows: nonSamplingPermits } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const systemUserId = 22; + + sinon.stub(permit_queries, 'getNonSamplingPermitsSQL').returns(SQL`some query`); + + const permitService = new PermitService(mockDBConnection); + const result = await permitService.getNonSamplingPermits(systemUserId); + + expect(result).to.eql(nonSamplingPermits); + }); + }); + + describe('createNoSamplePermits', () => { + const sampleReq = { + keycloak_token: {}, + body: { + coordinator: { + first_name: 'first', + last_name: 'last', + email_address: 'email@example.com', + coordinator_agency: 'agency', + share_contact_details: true + }, + permit: { + permits: [ + { + permit_number: 'number', + permit_type: 'type' + } + ] + } + } + } as any; + + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no permit passed in request body', async () => { + const mockDBConnection = getMockDBConnection(); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.createNoSamplePermits({ ...sampleReq.body, permit: null }); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing request body param `permit`'); + } + }); + + it('should throw a 400 error when no coordinator passed in request body', async () => { + const mockDBConnection = getMockDBConnection(); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.createNoSamplePermits({ ...sampleReq.body, coordinator: null }); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Missing request body param `coordinator`'); + } + }); + + it('should return the inserted ids on success', async () => { + const mockDBConnection = getMockDBConnection(); + + const permitService = new PermitService(mockDBConnection); + + sinon.stub(PermitService.prototype, 'insertNoSamplePermit').resolves(20); + + const result = await permitService.createNoSamplePermits(sampleReq.body); + + expect(result).to.eql([20]); + }); + + it('should throw an error when a failure occurs', async () => { + const expectedError = new Error('cannot process request'); + + const mockDBConnection = getMockDBConnection(); + + const permitService = new PermitService(mockDBConnection); + + sinon.stub(PermitService.prototype, 'insertNoSamplePermit').rejects(expectedError); + + try { + await permitService.createNoSamplePermits(sampleReq.body); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal(expectedError.message); + } + }); + }); + + describe('insertNoSamplePermit', () => { + afterEach(() => { + sinon.restore(); + }); + + const permitData = { + permit_number: 'number', + permit_type: 'type' + }; + + const coordinatorData = { + first_name: 'first', + last_name: 'last', + email_address: 'email@example.com', + coordinator_agency: 'agency', + share_contact_details: true + }; + + it('should throw an error when cannot generate post sql statement', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(permit_queries, 'postPermitNoSamplingSQL').returns(null); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.insertNoSamplePermit(permitData, coordinatorData); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); + } + }); + + it('should throw a HTTP 400 error when failed to insert non-sampling permits cause result is null', async () => { + const mockQueryResponse = (null as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(permit_queries, 'postPermitNoSamplingSQL').returns(SQL`some`); + + const permitService = new PermitService(mockDBConnection); + + try { + await permitService.insertNoSamplePermit(permitData, coordinatorData); + + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).status).to.equal(400); + expect((actualError as HTTPError).message).to.equal('Failed to insert non-sampling permit data'); + } + }); + + it('should return the result id on success', async () => { + const mockQueryResponse = ({ rows: [{ id: 12 }] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(permit_queries, 'postPermitNoSamplingSQL').returns(SQL`some`); + + const permitService = new PermitService(mockDBConnection); + + const res = await permitService.insertNoSamplePermit(permitData, coordinatorData); + + expect(res).to.equal(12); + }); + }); +}); diff --git a/api/src/services/permit-service.ts b/api/src/services/permit-service.ts new file mode 100644 index 0000000000..abec156a6b --- /dev/null +++ b/api/src/services/permit-service.ts @@ -0,0 +1,124 @@ +import { HTTP400 } from '../errors/custom-error'; +import { IPostPermitNoSampling, PostPermitNoSamplingObject } from '../models/permit-no-sampling'; +import { PostCoordinatorData } from '../models/project-create'; +import { PutCoordinatorData } from '../models/project-update'; +import { queries } from '../queries/queries'; +import { DBService } from './service'; + +interface IGetAllPermits { + id: string; + number: string; + type: string; + coordinator_agency: string; + project_name: string; +} + +interface IGetNonSamplingPermits { + permit_id: string; + number: string; + type: string; +} + +export class PermitService extends DBService { + /** + * get all non-sampling permits + * + * @param {(number | null)} systemUserId + * @return {*} {Promise} + * @memberof PermitService + */ + async getAllPermits(systemUserId: number | null): Promise { + const sqlStatement = queries.permit.getAllPermitsSQL(systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rows) { + throw new HTTP400('Failed to get all user permits'); + } + + return response.rows; + } + + /** + * get all non-sampling permits + * + * @param {(number | null)} systemUserId + * @return {*} {Promise} + * @memberof PermitService + */ + async getNonSamplingPermits(systemUserId: number | null): Promise { + const sqlStatement = queries.permit.getNonSamplingPermitsSQL(systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rows) { + throw new HTTP400('Failed to get all user permits'); + } + + return response.rows; + } + + /** + * Creates new no sample permit objects and insert all + * + * @param {object} permitRequestBody + * @return {*} {Promise} + * @memberof PermitService + */ + async createNoSamplePermits(permitRequestBody: object): Promise { + const sanitizedNoSamplePermitPostData = new PostPermitNoSamplingObject(permitRequestBody); + + if (!sanitizedNoSamplePermitPostData.permit || !sanitizedNoSamplePermitPostData.permit.permits.length) { + throw new HTTP400('Missing request body param `permit`'); + } + + if (!sanitizedNoSamplePermitPostData.coordinator) { + throw new HTTP400('Missing request body param `coordinator`'); + } + + return Promise.all( + sanitizedNoSamplePermitPostData.permit.permits.map(async (permit: IPostPermitNoSampling) => + this.insertNoSamplePermit(permit, sanitizedNoSamplePermitPostData.coordinator) + ) + ); + } + + /** + * insert a no sample permit row. + * + * @param {IPostPermitNoSampling} permit + * @param {(PostCoordinatorData | PutCoordinatorData)} coordinator + * @return {*} {Promise} + * @memberof PermitService + */ + async insertNoSamplePermit( + permit: IPostPermitNoSampling, + coordinator: PostCoordinatorData | PutCoordinatorData + ): Promise { + const systemUserId = this.connection.systemUserId(); + + const sqlStatement = queries.permit.postPermitNoSamplingSQL({ ...permit, ...coordinator }, systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert non-sampling permit data'); + } + + return result.id; + } +} diff --git a/api/src/services/project-service.test.ts b/api/src/services/project-service.test.ts new file mode 100644 index 0000000000..db5f7b8a6f --- /dev/null +++ b/api/src/services/project-service.test.ts @@ -0,0 +1,509 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import { HTTPError } from '../errors/custom-error'; +import { + GetCoordinatorData, + GetFundingData, + GetIUCNClassificationData, + GetLocationData, + GetObjectivesData, + GetPartnershipsData, + GetPermitData, + GetProjectData +} from '../models/project-view'; +import { queries } from '../queries/queries'; +import { getMockDBConnection } from '../__mocks__/db'; +import { ProjectService } from './project-service'; + +chai.use(sinonChai); + +describe('ProjectService', () => { + describe('ensureProjectParticipant', () => { + afterEach(() => { + sinon.restore(); + }); + + it('does not add a new project participant if one already exists', async () => { + const mockDBConnection = getMockDBConnection(); + + const getProjectParticipantStub = sinon + .stub(ProjectService.prototype, 'getProjectParticipant') + .resolves('existing participant'); + + const addProjectParticipantStub = sinon.stub(ProjectService.prototype, 'addProjectParticipant'); + + const projectId = 1; + const systemUserId = 1; + const projectParticipantRoleId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.ensureProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + } catch (actualError) { + expect.fail(); + } + + expect(getProjectParticipantStub).to.have.been.calledOnce; + expect(addProjectParticipantStub).not.to.have.been.called; + }); + + it('adds a new project participant if one did not already exist', async () => { + const mockDBConnection = getMockDBConnection(); + + const getProjectParticipantStub = sinon.stub(ProjectService.prototype, 'getProjectParticipant').resolves(null); + + const addProjectParticipantStub = sinon.stub(ProjectService.prototype, 'addProjectParticipant'); + + const projectId = 1; + const systemUserId = 1; + const projectParticipantRoleId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.ensureProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + } catch (actualError) { + expect.fail(); + } + + expect(getProjectParticipantStub).to.have.been.calledOnce; + expect(addProjectParticipantStub).to.have.been.calledOnce; + }); + }); + + describe('getProjectParticipant', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.projectParticipation, 'getProjectParticipationBySystemUserSQL').returns(null); + + const projectId = 1; + const systemUserId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getProjectParticipant(projectId, systemUserId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL select statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 response when response has no rowCount', async () => { + const mockQueryResponse = (null as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getProjectParticipationBySystemUserSQL').returns(SQL`valid sql`); + + const projectId = 1; + const systemUserId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getProjectParticipant(projectId, systemUserId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to get project team members'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns null if there are no rows', async () => { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getProjectParticipationBySystemUserSQL').returns(SQL`valid sql`); + + const projectId = 1; + const systemUserId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectParticipant(projectId, systemUserId); + + expect(result).to.equal(null); + }); + + it('returns the first row on success', async () => { + const mockRowObj = { id: 123 }; + const mockQueryResponse = ({ rows: [mockRowObj] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getProjectParticipationBySystemUserSQL').returns(SQL`valid sql`); + + const projectId = 1; + const systemUserId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectParticipant(projectId, systemUserId); + + expect(result).to.equal(mockRowObj); + }); + }); + + describe('getProjectParticipants', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.projectParticipation, 'getAllProjectParticipantsSQL').returns(null); + + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getProjectParticipants(projectId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL select statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 response when response has no rowCount', async () => { + const mockQueryResponse = (null as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getAllProjectParticipantsSQL').returns(SQL`valid sql`); + + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getProjectParticipants(projectId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to get project team members'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns empty array if there are no rows', async () => { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getAllProjectParticipantsSQL').returns(SQL`valid sql`); + + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectParticipants(projectId); + + expect(result).to.eql([]); + }); + + it('returns rows on success', async () => { + const mockRowObj = [{ id: 123 }]; + const mockQueryResponse = ({ rows: mockRowObj } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'getAllProjectParticipantsSQL').returns(SQL`valid sql`); + + const projectId = 1; + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectParticipants(projectId); + + expect(result).to.equal(mockRowObj); + }); + }); + + describe('addProjectParticipant', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.projectParticipation, 'addProjectRoleByRoleIdSQL').returns(null); + + const projectId = 1; + const systemUserId = 1; + const projectParticipantRoleId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.addProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL insert statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 response when response has no rowCount', async () => { + const mockQueryResponse = ({ rowCount: 0 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.projectParticipation, 'addProjectRoleByRoleIdSQL').returns(SQL`valid sql`); + + const projectId = 1; + const systemUserId = 1; + const projectParticipantRoleId = 1; + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.addProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to insert project team member'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should not throw an error on success', async () => { + const mockQueryResponse = ({ rowCount: 1 } as unknown) as QueryResult; + const mockQuery = sinon.fake.resolves(mockQueryResponse); + const mockDBConnection = getMockDBConnection({ query: mockQuery }); + + const addProjectRoleByRoleIdSQLStub = sinon + .stub(queries.projectParticipation, 'addProjectRoleByRoleIdSQL') + .returns(SQL`valid sql`); + + const projectId = 1; + const systemUserId = 1; + const projectParticipantRoleId = 1; + + const projectService = new ProjectService(mockDBConnection); + + await projectService.addProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + + expect(addProjectRoleByRoleIdSQLStub).to.have.been.calledOnce; + expect(mockQuery).to.have.been.calledOnce; + }); + }); + + describe('getPublicProjectsList', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.public, 'getPublicProjectListSQL').returns(null); + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getPublicProjectsList(); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns empty array if there are no rows', async () => { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.public, 'getPublicProjectListSQL').returns(SQL`valid sql`); + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getPublicProjectsList(); + + expect(result).to.eql([]); + }); + + it('returns rows on success', async () => { + const mockRowObj = [ + { + id: 123, + name: 'Project 1', + start_date: '1900-01-01', + end_date: '2000-10-10', + coordinator_agency: 'Agency 1', + permits_list: '3, 100', + project_type: 'Aquatic Habitat' + }, + { + id: 456, + name: 'Project 2', + start_date: '1900-01-01', + end_date: '2000-12-31', + coordinator_agency: 'Agency 2', + permits_list: '1, 4', + project_type: 'Terrestrial Habitat' + } + ]; + const mockQueryResponse = ({ rows: mockRowObj } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.public, 'getPublicProjectListSQL').returns(SQL`valid sql`); + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getPublicProjectsList(); + + expect(result[0].id).to.equal(123); + expect(result[0].name).to.equal('Project 1'); + expect(result[0].completion_status).to.equal('Completed'); + + expect(result[1].id).to.equal(456); + expect(result[1].name).to.equal('Project 2'); + expect(result[1].completion_status).to.equal('Completed'); + }); + }); + + describe('getProjectList', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.project, 'getProjectListSQL').returns(null); + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getProjectList(true, 1, {}); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL select statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('returns empty array if there are no rows', async () => { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.project, 'getProjectListSQL').returns(SQL`valid sql`); + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectList(true, 1, {}); + + expect(result).to.eql([]); + }); + + it('returns rows on success', async () => { + const mockRowObj = [ + { + id: 123, + name: 'Project 1', + start_date: '1900-01-01', + end_date: '2200-10-10', + coordinator_agency: 'Agency 1', + publish_timestamp: '2010-01-01', + permits_list: '3, 100', + project_type: 'Aquatic Habitat' + }, + { + id: 456, + name: 'Project 2', + start_date: '1900-01-01', + end_date: '2000-12-31', + coordinator_agency: 'Agency 2', + publish_timestamp: '', + permits_list: '1, 4', + project_type: 'Terrestrial Habitat' + } + ]; + const mockQueryResponse = ({ rows: mockRowObj } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(queries.project, 'getProjectListSQL').returns(SQL`valid sql`); + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getProjectList(true, 1, {}); + + expect(result[0].id).to.equal(123); + expect(result[0].name).to.equal('Project 1'); + expect(result[0].completion_status).to.equal('Active'); + expect(result[0].publish_status).to.equal('Published'); + + expect(result[1].id).to.equal(456); + expect(result[1].name).to.equal('Project 2'); + expect(result[1].completion_status).to.equal('Completed'); + expect(result[1].publish_status).to.equal('Unpublished'); + }); + }); + + describe('getPublicProjectById', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw a 400 error when no sql statement produced for getPublicProjectSQL', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.public, 'getPublicProjectSQL').returns(null); + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getPublicProjectById(1); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + + it('should throw a 400 error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + sinon.stub(queries.public, 'getPublicProjectSQL').returns(null); + sinon.stub(queries.public, 'getActivitiesByPublicProjectSQL').returns(null); + + const projectService = new ProjectService(mockDBConnection); + + try { + await projectService.getPublicProjectById(1); + expect.fail(); + } catch (actualError) { + expect((actualError as HTTPError).message).to.equal('Failed to build SQL get statement'); + expect((actualError as HTTPError).status).to.equal(400); + } + }); + }); + + it('returns rows on success', async () => { + const mockQueryResponse = ({ rows: [{ id: 1 }] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + sinon.stub(ProjectService.prototype, 'getPublicProjectData').resolves(new GetProjectData()); + sinon.stub(ProjectService.prototype, 'getObjectivesData').resolves(new GetObjectivesData()); + sinon.stub(ProjectService.prototype, 'getCoordinatorData').resolves(new GetCoordinatorData()); + sinon.stub(ProjectService.prototype, 'getPermitData').resolves(new GetPermitData()); + sinon.stub(ProjectService.prototype, 'getLocationData').resolves(new GetLocationData()); + sinon.stub(ProjectService.prototype, 'getPartnershipsData').resolves(new GetPartnershipsData()); + sinon.stub(ProjectService.prototype, 'getIUCNClassificationData').resolves(new GetIUCNClassificationData()); + sinon.stub(ProjectService.prototype, 'getFundingData').resolves(new GetFundingData()); + + const projectService = new ProjectService(mockDBConnection); + + const result = await projectService.getPublicProjectById(1); + + expect(result.id).to.equal(1); + }); +}); diff --git a/api/src/services/project-service.ts b/api/src/services/project-service.ts new file mode 100644 index 0000000000..17c59c6a57 --- /dev/null +++ b/api/src/services/project-service.ts @@ -0,0 +1,1175 @@ +import moment from 'moment'; +import SQL from 'sql-template-strings'; +import { PROJECT_ROLE, SYSTEM_ROLE } from '../constants/roles'; +import { COMPLETION_STATUS } from '../constants/status'; +import { HTTP400, HTTP409, HTTP500 } from '../errors/custom-error'; +import { + IPostExistingPermit, + IPostIUCN, + IPostPermit, + PostFundingSource, + PostPermitData, + PostProjectObject +} from '../models/project-create'; +import { + IPutIUCN, + PutCoordinatorData, + PutFundingSource, + PutIUCNData, + PutLocationData, + PutObjectivesData, + PutPartnershipsData, + PutProjectData +} from '../models/project-update'; +import { + GetCoordinatorData, + GetFundingData, + GetIUCNClassificationData, + GetLocationData, + GetObjectivesData, + GetPartnershipsData, + GetPermitData, + GetProjectData, + GetSpeciesData, + IGetProject +} from '../models/project-view'; +import { getSurveyAttachmentS3Keys } from '../paths/project/{projectId}/survey/{surveyId}/delete'; +import { GET_ENTITIES, IUpdateProject } from '../paths/project/{projectId}/update'; +import { queries } from '../queries/queries'; +import { userHasValidRole } from '../request-handlers/security/authorization'; +import { deleteFileFromS3 } from '../utils/file-utils'; +import { DBService } from './service'; +import { TaxonomyService } from './taxonomy-service'; + +export class ProjectService extends DBService { + /** + * Gets the project participant, adding them if they do not already exist. + * + * @param {number} projectId + * @param {number} systemUserId + * @return {*} {Promise} + * @memberof ProjectService + */ + async ensureProjectParticipant( + projectId: number, + systemUserId: number, + projectParticipantRoleId: number + ): Promise { + const projectParticipantRecord = await this.getProjectParticipant(projectId, systemUserId); + + if (projectParticipantRecord) { + // project participant already exists, do nothing + return; + } + + // add new project participant record + await this.addProjectParticipant(projectId, systemUserId, projectParticipantRoleId); + } + + /** + * Get an existing project participant. + * + * @param {number} projectId + * @param {number} systemUserId + * @return {*} {Promise} + * @memberof ProjectService + */ + async getProjectParticipant(projectId: number, systemUserId: number): Promise { + const sqlStatement = queries.projectParticipation.getProjectParticipationBySystemUserSQL(projectId, systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response) { + throw new HTTP400('Failed to get project team members'); + } + + return response?.rows?.[0] || null; + } + + /** + * Get all project participants for a project. + * + * @param {number} projectId + * @return {*} {Promise} + * @memberof ProjectService + */ + async getProjectParticipants(projectId: number): Promise { + const sqlStatement = queries.projectParticipation.getAllProjectParticipantsSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rows) { + throw new HTTP400('Failed to get project team members'); + } + + return (response && response.rows) || []; + } + + /** + * Adds a new project participant. + * + * Note: Will fail if the project participant already exists. + * + * @param {number} projectId + * @param {number} systemUserId + * @param {number} projectParticipantRoleId + * @return {*} {Promise} + * @memberof ProjectService + */ + async addProjectParticipant( + projectId: number, + systemUserId: number, + projectParticipantRoleId: number + ): Promise { + const sqlStatement = queries.projectParticipation.addProjectRoleByRoleIdSQL( + projectId, + systemUserId, + projectParticipantRoleId + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to insert project team member'); + } + } + + async getPublicProjectsList(): Promise { + const getProjectListSQLStatement = queries.public.getPublicProjectListSQL(); + + if (!getProjectListSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(getProjectListSQLStatement.text, getProjectListSQLStatement.values); + + if (!response || !response.rows || !response.rows.length) { + return []; + } + + return response.rows.map((row) => ({ + id: row.id, + name: row.name, + start_date: row.start_date, + end_date: row.end_date, + coordinator_agency: row.coordinator_agency, + completion_status: + (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || + COMPLETION_STATUS.ACTIVE, + project_type: row.project_type, + permits_list: row.permits_list + })); + } + + async getPublicProjectById(projectId: number): Promise { + const [ + projectData, + objectiveData, + coordinatorData, + permitData, + locationData, + iucnData, + fundingData, + partnershipsData + ] = await Promise.all([ + this.getPublicProjectData(projectId), + this.getObjectivesData(projectId), + this.getCoordinatorData(projectId), + this.getPermitData(projectId), + this.getLocationData(projectId), + this.getIUCNClassificationData(projectId), + this.getFundingData(projectId), + this.getPartnershipsData(projectId) + ]); + + return { + id: projectId, + project: projectData, + objectives: objectiveData, + coordinator: coordinatorData, + permit: permitData, + location: locationData, + iucn: iucnData, + funding: fundingData, + partnerships: partnershipsData + }; + } + + async getProjectList(isUserAdmin: boolean, systemUserId: number | null, filterFields: any): Promise { + const sqlStatement = queries.project.getProjectListSQL(isUserAdmin, systemUserId, filterFields); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rows) { + return []; + } + + return response.rows.map((row) => ({ + id: row.id, + name: row.name, + start_date: row.start_date, + end_date: row.end_date, + coordinator_agency: row.coordinator_agency_name, + publish_status: row.publish_timestamp ? 'Published' : 'Unpublished', + completion_status: + (row.end_date && moment(row.end_date).endOf('day').isBefore(moment()) && COMPLETION_STATUS.COMPLETED) || + COMPLETION_STATUS.ACTIVE, + project_type: row.project_type, + permits_list: row.permits_list + })); + } + + async getProjectById(projectId: number): Promise { + const [ + projectData, + objectiveData, + coordinatorData, + permitData, + locationData, + iucnData, + fundingData, + partnershipsData + ] = await Promise.all([ + this.getProjectData(projectId), + this.getObjectivesData(projectId), + this.getCoordinatorData(projectId), + this.getPermitData(projectId), + this.getLocationData(projectId), + this.getIUCNClassificationData(projectId), + this.getFundingData(projectId), + this.getPartnershipsData(projectId) + ]); + + return { + id: projectId, + project: projectData, + objectives: objectiveData, + coordinator: coordinatorData, + permit: permitData, + location: locationData, + iucn: iucnData, + funding: fundingData, + partnerships: partnershipsData + }; + } + + async getProjectEntitiesById(projectId: number, entities: string[]): Promise { + const results: IGetProject = { + id: projectId, + coordinator: null, + permit: null, + project: null, + objectives: null, + location: null, + iucn: null, + funding: null, + partnerships: null + }; + + const promises: Promise[] = []; + + if (entities.includes(GET_ENTITIES.coordinator)) { + promises.push( + this.getCoordinatorData(projectId).then((value) => { + results.coordinator = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.permit)) { + promises.push( + this.getPermitData(projectId).then((value) => { + results.permit = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.partnerships)) { + promises.push( + this.getPartnershipsData(projectId).then((value) => { + results.partnerships = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.location)) { + promises.push( + this.getLocationData(projectId).then((value) => { + results.location = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.iucn)) { + promises.push( + this.getIUCNClassificationData(projectId).then((value) => { + results.iucn = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.objectives)) { + promises.push( + this.getObjectivesData(projectId).then((value) => { + results.objectives = value; + }) + ); + } + + if (entities.includes(GET_ENTITIES.project)) { + promises.push( + this.getProjectData(projectId).then((value) => { + results.project = value; + }) + ); + } + if (entities.includes(GET_ENTITIES.funding)) { + promises.push( + this.getProjectData(projectId).then((value) => { + results.project = value; + }) + ); + } + + await Promise.all(promises); + + return results; + } + + async getProjectData(projectId: number): Promise { + const getProjectSqlStatement = queries.project.getProjectSQL(projectId); + const getProjectActivitiesSQLStatement = queries.project.getActivitiesByProjectSQL(projectId); + + if (!getProjectSqlStatement || !getProjectActivitiesSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const [project, activity] = await Promise.all([ + this.connection.query(getProjectSqlStatement.text, getProjectSqlStatement.values), + this.connection.query(getProjectActivitiesSQLStatement.text, getProjectActivitiesSQLStatement.values) + ]); + + const projectResult = (project && project.rows && project.rows[0]) || null; + const activityResult = (activity && activity.rows) || null; + + if (!projectResult || !activityResult) { + throw new HTTP400('Failed to get project data'); + } + + return new GetProjectData(projectResult, activityResult); + } + + async getObjectivesData(projectId: number): Promise { + const sqlStatement = queries.project.getObjectivesByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result) { + throw new HTTP400('Failed to get project objectives data'); + } + + return new GetObjectivesData(result); + } + + async getCoordinatorData(projectId: number): Promise { + const sqlStatement = queries.project.getCoordinatorByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result) { + throw new HTTP400('Failed to get project contact data'); + } + + return new GetCoordinatorData(result); + } + + async getPermitData(projectId: number): Promise { + const sqlStatement = queries.project.getProjectPermitsSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get project permit data'); + } + + return new GetPermitData(result); + } + + async getLocationData(projectId: number): Promise { + const sqlStatement = queries.project.getLocationByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get project data'); + } + + return new GetLocationData(result); + } + + async getIUCNClassificationData(projectId: number): Promise { + const sqlStatement = queries.project.getIUCNActionClassificationByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get project data'); + } + + return new GetIUCNClassificationData(result); + } + + async getFundingData(projectId: number): Promise { + const sqlStatement = queries.project.getFundingSourceByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get project data'); + } + + return new GetFundingData(result); + } + + async getPartnershipsData(projectId: number): Promise { + const [indigenousPartnershipsRows, stakegholderPartnershipsRows] = await Promise.all([ + this.getIndigenousPartnershipsRows(projectId), + this.getStakeholderPartnershipsRows(projectId) + ]); + + if (!indigenousPartnershipsRows) { + throw new HTTP400('Failed to get indigenous partnership data'); + } + + if (!stakegholderPartnershipsRows) { + throw new HTTP400('Failed to get stakeholder partnership data'); + } + + return new GetPartnershipsData(indigenousPartnershipsRows, stakegholderPartnershipsRows); + } + + async getIndigenousPartnershipsRows(projectId: number): Promise { + const sqlStatement = queries.project.getIndigenousPartnershipsByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + return (response && response.rows) || null; + } + + async getStakeholderPartnershipsRows(projectId: number): Promise { + const sqlStatement = queries.project.getStakeholderPartnershipsByProjectSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + return (response && response.rows) || null; + } + + async createProject(postProjectData: PostProjectObject): Promise { + const projectId = await this.insertProject(postProjectData); + + const promises: Promise[] = []; + + // Handle funding sources + promises.push( + Promise.all( + postProjectData.funding.funding_sources.map((fundingSource: PostFundingSource) => + this.insertFundingSource(fundingSource, projectId) + ) + ) + ); + + // Handle indigenous partners + promises.push( + Promise.all( + postProjectData.partnerships.indigenous_partnerships.map((indigenousNationId: number) => + this.insertIndigenousNation(indigenousNationId, projectId) + ) + ) + ); + + // Handle stakeholder partners + promises.push( + Promise.all( + postProjectData.partnerships.stakeholder_partnerships.map((stakeholderPartner: string) => + this.insertStakeholderPartnership(stakeholderPartner, projectId) + ) + ) + ); + + // Handle new project permits + promises.push( + Promise.all( + postProjectData.permit.permits.map((permit: IPostPermit) => + this.insertPermit(permit.permit_number, permit.permit_type, projectId) + ) + ) + ); + + // Handle existing non-sampling permits which are now being associated to a project + promises.push( + Promise.all( + postProjectData.permit.existing_permits.map((existing_permit: IPostExistingPermit) => + this.associateExistingPermitToProject(existing_permit.permit_id, projectId) + ) + ) + ); + + // Handle project IUCN classifications + promises.push( + Promise.all( + postProjectData.iucn.classificationDetails.map((classificationDetail: IPostIUCN) => + this.insertClassificationDetail(classificationDetail.subClassification2, projectId) + ) + ) + ); + + // Handle project activities + promises.push( + Promise.all( + postProjectData.project.project_activities.map((activityId: number) => + this.insertActivity(activityId, projectId) + ) + ) + ); + + await Promise.all(promises); + + // The user that creates a project is automatically assigned a project lead role, for this project + await this.insertParticipantRole(projectId, PROJECT_ROLE.PROJECT_LEAD); + + return projectId; + } + + async insertProject(postProjectData: PostProjectObject): Promise { + const sqlStatement = queries.project.postProjectSQL({ + ...postProjectData.project, + ...postProjectData.location, + ...postProjectData.objectives, + ...postProjectData.coordinator + }); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project boundary data'); + } + + return result.id; + } + + async insertFundingSource(fundingSource: PostFundingSource, project_id: number): Promise { + const sqlStatement = queries.project.postProjectFundingSourceSQL(fundingSource, project_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project funding data'); + } + + return result.id; + } + + async insertIndigenousNation(indigenousNationsId: number, project_id: number): Promise { + const sqlStatement = queries.project.postProjectIndigenousNationSQL(indigenousNationsId, project_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project first nations partnership data'); + } + + return result.id; + } + + async insertStakeholderPartnership(stakeholderPartner: string, project_id: number): Promise { + const sqlStatement = queries.project.postProjectStakeholderPartnershipSQL(stakeholderPartner, project_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project stakeholder partnership data'); + } + + return result.id; + } + + async insertPermit(permitNumber: string, permitType: string, projectId: number): Promise { + const systemUserId = this.connection.systemUserId(); + + if (!systemUserId) { + throw new HTTP400('Failed to identify system user ID'); + } + + const sqlStatement = queries.permit.postProjectPermitSQL(permitNumber, permitType, projectId, systemUserId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project permit data'); + } + + return result.id; + } + + async associateExistingPermitToProject(permitId: number, projectId: number): Promise { + const sqlStatement = queries.permit.associatePermitToProjectSQL(permitId, projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement for associatePermitToProjectSQL'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rowCount) || null; + + if (!result) { + throw new HTTP400('Failed to associate existing permit to project'); + } + } + + async insertClassificationDetail(iucn3_id: number, project_id: number): Promise { + const sqlStatement = queries.project.postProjectIUCNSQL(iucn3_id, project_id); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project IUCN data'); + } + + return result.id; + } + + async insertActivity(activityId: number, projectId: number): Promise { + const sqlStatement = queries.project.postProjectActivitySQL(activityId, projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result || !result.id) { + throw new HTTP400('Failed to insert project activity data'); + } + + return result.id; + } + + async insertParticipantRole(projectId: number, projectParticipantRole: string): Promise { + const systemUserId = this.connection.systemUserId(); + + if (!systemUserId) { + throw new HTTP400('Failed to identify system user ID'); + } + + const sqlStatement = queries.projectParticipation.addProjectRoleByRoleNameSQL( + projectId, + systemUserId, + projectParticipantRole + ); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response || !response.rowCount) { + throw new HTTP400('Failed to insert project team member'); + } + } + + async updateProject(projectId: number, entities: IUpdateProject) { + const promises: Promise[] = []; + + if (entities?.partnerships) { + promises.push(this.updatePartnershipsData(projectId, entities)); + } + + if (entities?.project || entities?.location || entities?.objectives || entities?.coordinator) { + promises.push(this.updateProjectData(projectId, entities)); + } + + if (entities?.permit && entities?.coordinator) { + promises.push(this.updatePermitData(projectId, entities)); + } + + if (entities?.iucn) { + promises.push(this.updateIUCNData(projectId, entities)); + } + + if (entities?.funding) { + promises.push(this.updateFundingData(projectId, entities)); + } + + await Promise.all(promises); + } + + async updatePermitData(projectId: number, entities: IUpdateProject): Promise { + if (!entities.permit) { + throw new HTTP400('Missing request body entity `permit`'); + } + + const putPermitData = new PostPermitData(entities.permit); + + const sqlDeleteStatement = queries.project.deletePermitSQL(projectId); + + if (!sqlDeleteStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteResult = await this.connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); + + if (!deleteResult) { + throw new HTTP409('Failed to delete project permit data'); + } + + const insertPermitPromises = + putPermitData?.permits?.map((permit: IPostPermit) => { + return this.insertPermit(permit.permit_number, permit.permit_type, projectId); + }) || []; + + // Handle existing non-sampling permits which are now being associated to a project + const updateExistingPermitPromises = + putPermitData?.existing_permits?.map((existing_permit: IPostExistingPermit) => { + return this.associateExistingPermitToProject(existing_permit.permit_id, projectId); + }) || []; + + await Promise.all([insertPermitPromises, updateExistingPermitPromises]); + } + + async updateIUCNData(projectId: number, entities: IUpdateProject): Promise { + const putIUCNData = (entities?.iucn && new PutIUCNData(entities.iucn)) || null; + + const sqlDeleteStatement = queries.project.deleteIUCNSQL(projectId); + + if (!sqlDeleteStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteResult = await this.connection.query(sqlDeleteStatement.text, sqlDeleteStatement.values); + + if (!deleteResult) { + throw new HTTP409('Failed to delete project IUCN data'); + } + + const insertIUCNPromises = + putIUCNData?.classificationDetails?.map((iucnClassification: IPutIUCN) => + this.insertClassificationDetail(iucnClassification.subClassification2, projectId) + ) || []; + + await Promise.all(insertIUCNPromises); + } + + async updatePartnershipsData(projectId: number, entities: IUpdateProject): Promise { + const putPartnershipsData = (entities?.partnerships && new PutPartnershipsData(entities.partnerships)) || null; + + const sqlDeleteIndigenousPartnershipsStatement = queries.project.deleteIndigenousPartnershipsSQL(projectId); + const sqlDeleteStakeholderPartnershipsStatement = queries.project.deleteStakeholderPartnershipsSQL(projectId); + + if (!sqlDeleteIndigenousPartnershipsStatement || !sqlDeleteStakeholderPartnershipsStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteIndigenousPartnershipsPromises = this.connection.query( + sqlDeleteIndigenousPartnershipsStatement.text, + sqlDeleteIndigenousPartnershipsStatement.values + ); + + const deleteStakeholderPartnershipsPromises = this.connection.query( + sqlDeleteStakeholderPartnershipsStatement.text, + sqlDeleteStakeholderPartnershipsStatement.values + ); + + const [deleteIndigenousPartnershipsResult, deleteStakeholderPartnershipsResult] = await Promise.all([ + deleteIndigenousPartnershipsPromises, + deleteStakeholderPartnershipsPromises + ]); + + if (!deleteIndigenousPartnershipsResult) { + throw new HTTP409('Failed to delete project indigenous partnerships data'); + } + + if (!deleteStakeholderPartnershipsResult) { + throw new HTTP409('Failed to delete project stakeholder partnerships data'); + } + + const insertIndigenousPartnershipsPromises = + putPartnershipsData?.indigenous_partnerships?.map((indigenousPartnership: number) => + this.insertIndigenousNation(indigenousPartnership, projectId) + ) || []; + + const insertStakeholderPartnershipsPromises = + putPartnershipsData?.stakeholder_partnerships?.map((stakeholderPartnership: string) => + this.insertStakeholderPartnership(stakeholderPartnership, projectId) + ) || []; + + await Promise.all([...insertIndigenousPartnershipsPromises, ...insertStakeholderPartnershipsPromises]); + } + + async updateProjectData(projectId: number, entities: IUpdateProject): Promise { + const putProjectData = (entities?.project && new PutProjectData(entities.project)) || null; + const putLocationData = (entities?.location && new PutLocationData(entities.location)) || null; + const putObjectivesData = (entities?.objectives && new PutObjectivesData(entities.objectives)) || null; + const putCoordinatorData = (entities?.coordinator && new PutCoordinatorData(entities.coordinator)) || null; + + // Update project table + const revision_count = + putProjectData?.revision_count ?? + putLocationData?.revision_count ?? + putObjectivesData?.revision_count ?? + putCoordinatorData?.revision_count ?? + null; + + if (!revision_count && revision_count !== 0) { + throw new HTTP400('Failed to parse request body'); + } + + const sqlUpdateProject = queries.project.putProjectSQL( + projectId, + putProjectData, + putLocationData, + putObjectivesData, + putCoordinatorData, + revision_count + ); + + if (!sqlUpdateProject) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const result = await this.connection.query(sqlUpdateProject.text, sqlUpdateProject.values); + + if (!result || !result.rowCount) { + // TODO if revision count is bad, it is supposed to raise an exception? + // It currently does skip the update as expected, but it just returns 0 rows updated, and doesn't result in any errors + throw new HTTP409('Failed to update stale project data'); + } + + if (putProjectData?.project_activities.length) { + await this.updateActivityData(projectId, putProjectData); + } + } + + async updateActivityData(projectId: number, projectData: PutProjectData) { + const sqlDeleteActivities = queries.project.deleteActivitiesSQL(projectId); + + if (!sqlDeleteActivities) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const deleteActivitiesResult = await this.connection.query(sqlDeleteActivities.text, sqlDeleteActivities.values); + + if (!deleteActivitiesResult) { + throw new HTTP409('Failed to update project activity data'); + } + + const insertActivityPromises = + projectData?.project_activities?.map((activityId: number) => this.insertActivity(activityId, projectId)) || []; + + await Promise.all([...insertActivityPromises]); + } + + async updateFundingData(projectId: number, entities: IUpdateProject): Promise { + const putFundingSource = entities?.funding && new PutFundingSource(entities.funding); + + const surveyFundingSourceDeleteStatement = queries.survey.deleteSurveyFundingSourceByProjectFundingSourceIdSQL( + putFundingSource?.id + ); + const projectFundingSourceDeleteStatement = queries.project.deleteProjectFundingSourceSQL( + projectId, + putFundingSource?.id + ); + + if (!projectFundingSourceDeleteStatement || !surveyFundingSourceDeleteStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + const surveyFundingSourceDeleteResult = await this.connection.query( + surveyFundingSourceDeleteStatement.text, + surveyFundingSourceDeleteStatement.values + ); + + if (!surveyFundingSourceDeleteResult) { + throw new HTTP409('Failed to delete survey funding source'); + } + + const projectFundingSourceDeleteResult = await this.connection.query( + projectFundingSourceDeleteStatement.text, + projectFundingSourceDeleteStatement.values + ); + + if (!projectFundingSourceDeleteResult) { + throw new HTTP409('Failed to delete project funding source'); + } + + const sqlInsertStatement = queries.project.putProjectFundingSourceSQL(putFundingSource, projectId); + + if (!sqlInsertStatement) { + throw new HTTP400('Failed to build SQL insert statement'); + } + + const insertResult = await this.connection.query(sqlInsertStatement.text, sqlInsertStatement.values); + + if (!insertResult) { + throw new HTTP409('Failed to put (insert) project funding source with incremented revision count'); + } + } + + async updatePublishStatus(projectId: number, publish: boolean): Promise { + const sqlStatement = queries.project.updateProjectPublishStatusSQL(projectId, publish); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + const result = (response && response.rows && response.rows[0]) || null; + + if (!response || !result) { + throw new HTTP500('Failed to update project publish status'); + } + + return result.id; + } + + async deleteProject(projectId: number, userRoles: string | string[]): Promise { + /** + * PART 1 + * Check that user is a system administrator - can delete a project (published or not) + * Check that user is a project administrator - can delete a project (unpublished only) + * + */ + const getProjectSQLStatement = queries.project.getProjectSQL(projectId); + + if (!getProjectSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const projectData = await this.connection.query(getProjectSQLStatement.text, getProjectSQLStatement.values); + + const projectResult = (projectData && projectData.rows && projectData.rows[0]) || null; + + if (!projectResult || !projectResult.id) { + throw new HTTP400('Failed to get the project'); + } + + if (projectResult.publish_date && userHasValidRole([SYSTEM_ROLE.PROJECT_CREATOR], userRoles)) { + throw new HTTP400('Cannot delete a published project if you are not a system administrator.'); + } + + /** + * PART 2 + * Get the attachment S3 keys for all attachments associated to this project and surveys under this project + * Used to delete them from S3 separately later + */ + const getProjectAttachmentSQLStatement = queries.project.getProjectAttachmentsSQL(projectId); + const getSurveyIdsSQLStatement = queries.survey.getSurveyIdsSQL(projectId); + + if (!getProjectAttachmentSQLStatement || !getSurveyIdsSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const getProjectAttachmentsResult = await this.connection.query( + getProjectAttachmentSQLStatement.text, + getProjectAttachmentSQLStatement.values + ); + + if (!getProjectAttachmentsResult || !getProjectAttachmentsResult.rows) { + throw new HTTP400('Failed to get project attachments'); + } + + const getSurveyIdsResult = await this.connection.query( + getSurveyIdsSQLStatement.text, + getSurveyIdsSQLStatement.values + ); + + if (!getSurveyIdsResult || !getSurveyIdsResult.rows) { + throw new HTTP400('Failed to get survey ids associated to project'); + } + + const surveyAttachmentS3Keys: string[] = Array.prototype.concat.apply( + [], + await Promise.all( + getSurveyIdsResult.rows.map((survey: any) => getSurveyAttachmentS3Keys(survey.id, this.connection)) + ) + ); + + const projectAttachmentS3Keys: string[] = getProjectAttachmentsResult.rows.map((attachment: any) => { + return attachment.key; + }); + + /** + * PART 3 + * Delete the project and all associated records/resources from our DB + */ + const deleteProjectSQLStatement = queries.project.deleteProjectSQL(projectId); + + if (!deleteProjectSQLStatement) { + throw new HTTP400('Failed to build SQL delete statement'); + } + + await this.connection.query(deleteProjectSQLStatement.text, deleteProjectSQLStatement.values); + + /** + * PART 4 + * Delete the project and survey attachments from S3 + */ + const deleteResult = [ + ...(await Promise.all(projectAttachmentS3Keys.map((projectS3Key: string) => deleteFileFromS3(projectS3Key)))), + ...(await Promise.all(surveyAttachmentS3Keys.map((surveyS3Key: string) => deleteFileFromS3(surveyS3Key)))) + ]; + + if (deleteResult.some((deleteResult) => !deleteResult)) { + return null; + } + + return true; + } + + async getPublicProjectData(projectId: number): Promise { + const getProjectSqlStatement = queries.public.getPublicProjectSQL(projectId); + const getProjectActivitiesSQLStatement = queries.public.getActivitiesByPublicProjectSQL(projectId); + + if (!getProjectSqlStatement || !getProjectActivitiesSQLStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const [project, activity] = await Promise.all([ + this.connection.query(getProjectSqlStatement.text, getProjectSqlStatement.values), + this.connection.query(getProjectActivitiesSQLStatement.text, getProjectActivitiesSQLStatement.values) + ]); + + const projectResult = (project && project.rows && project.rows[0]) || null; + const activityResult = (activity && activity.rows) || null; + + if (!projectResult || !activityResult) { + throw new HTTP400('Failed to get project data'); + } + + return new GetProjectData(projectResult, activityResult); + } + + async getSpeciesData(projectId: number): Promise { + const sqlStatement = SQL` + SELECT + wldtaxonomic_units_id + FROM + project_species + WHERE + project_id = ${projectId}; + `; + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get species data'); + } + + const taxonomyService = new TaxonomyService(); + + const species = await taxonomyService.getSpeciesFromIds(result); + + return new GetSpeciesData(species); + } +} diff --git a/api/src/services/service.ts b/api/src/services/service.ts new file mode 100644 index 0000000000..5e2c701627 --- /dev/null +++ b/api/src/services/service.ts @@ -0,0 +1,15 @@ +import { IDBConnection } from '../database/db'; + +/** + * Base class for services that require a database connection. + * + * @export + * @class DBService + */ +export class DBService { + connection: IDBConnection; + + constructor(connection: IDBConnection) { + this.connection = connection; + } +} diff --git a/api/src/services/survey-service.ts b/api/src/services/survey-service.ts new file mode 100644 index 0000000000..fd5e6c8e9b --- /dev/null +++ b/api/src/services/survey-service.ts @@ -0,0 +1,101 @@ +import SQL from 'sql-template-strings'; +import { HTTP400 } from '../errors/custom-error'; +import { GetSpeciesData, GetSurveyData, SurveyObject } from '../models/survey-view'; +import { queries } from '../queries/queries'; +import { DBService } from './service'; +import { TaxonomyService } from './taxonomy-service'; + +export class SurveyService extends DBService { + async getSurveyIdsByProjectId(projectId: number): Promise { + const sqlStatement = queries.survey.getSurveyIdsSQL(projectId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rows) { + return []; + } + + return response.rows; + } + + async getSurveyById(surveyId: number): Promise { + const [surveyData, speciesData] = await Promise.all([this.getSurveyData(surveyId), this.getSpeciesData(surveyId)]); + + return { + survey: surveyData, + species: speciesData + }; + } + + async getSurveyData(surveyId: number): Promise { + const sqlStatement = queries.survey.getSurveySQL(surveyId); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL get statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows && response.rows[0]) || null; + + if (!result) { + throw new HTTP400('Failed to get project data'); + } + + return new GetSurveyData(result); + } + + async getSpeciesData(surveyId: number): Promise { + const sqlStatement = SQL` + SELECT + wldtaxonomic_units_id + FROM + study_species + WHERE + survey_id = ${surveyId}; + `; + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + const result = (response && response.rows) || null; + + if (!result) { + throw new HTTP400('Failed to get species data'); + } + + const taxonomyService = new TaxonomyService(); + + const species = await taxonomyService.getSpeciesFromIds(result); + + return new GetSpeciesData(species); + } + + /** + * Get surveys by their ids. + * + * @param {number[]} surveyIds + * @param {boolean} [isPublic=false] Set to `true` if the return value should not include data that is not meant for + * public consumption. + * @return {*} {Promise< + * { + * survey: GetSurveyData; + * species: GetSpeciesData; + * }[] + * >} + * @memberof SurveyService + */ + async getSurveysByIds( + surveyIds: number[] + ): Promise< + { + survey: GetSurveyData; + species: GetSpeciesData; + }[] + > { + return Promise.all(surveyIds.map(async (surveyId) => this.getSurveyById(surveyId))); + } +} diff --git a/api/src/services/taxonomy-service.test.ts b/api/src/services/taxonomy-service.test.ts new file mode 100644 index 0000000000..d671d203be --- /dev/null +++ b/api/src/services/taxonomy-service.test.ts @@ -0,0 +1,14 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinonChai from 'sinon-chai'; +import { TaxonomyService } from './taxonomy-service'; + +chai.use(sinonChai); + +describe('TaxonomyService', () => { + it('constructs', () => { + const taxonomyService = new TaxonomyService(); + + expect(taxonomyService).to.be.instanceof(TaxonomyService); + }); +}); diff --git a/api/src/services/taxonomy-service.ts b/api/src/services/taxonomy-service.ts new file mode 100644 index 0000000000..c9a88bb3b6 --- /dev/null +++ b/api/src/services/taxonomy-service.ts @@ -0,0 +1,99 @@ +import { Client } from '@elastic/elasticsearch'; +import { SearchHit, SearchRequest } from '@elastic/elasticsearch/lib/api/types'; +import { getLogger } from '../utils/logger'; + +const defaultLog = getLogger('services/taxonomy-service'); + +export class TaxonomyService { + private async elasticSearch(searchRequest: SearchRequest) { + try { + const client = new Client({ node: process.env.ELASTICSEARCH_URL }); + return await client.search({ + index: 'taxonomy', + ...searchRequest + }); + } catch (error) { + defaultLog.debug({ label: 'elasticSearch', message: 'error', error }); + } + } + + private sanitizeSpeciesData = (data: SearchHit[]) => { + return data.map((item) => { + const label = [ + item._source.code, + [ + [item._source.tty_kingdom, item._source.tty_name].filter(Boolean).join(' '), + [item._source.unit_name1, item._source.unit_name2, item._source.unit_name3].filter(Boolean).join(' '), + item._source.english_name + ] + .filter(Boolean) + .join(', ') + ] + .filter(Boolean) + .join(': '); + + return { id: item._id, label: label }; + }); + }; + + async getTaxonomyFromIds(ids: number[]) { + const response = await this.elasticSearch({ + query: { + terms: { + _id: ids + } + } + }); + + return (response && response.hits.hits.map((item) => item._source)) || []; + } + + async getSpeciesFromIds(ids: string[]) { + const response = await this.elasticSearch({ + query: { + terms: { + _id: ids + } + } + }); + + return response ? this.sanitizeSpeciesData(response.hits.hits) : []; + } + + async searchSpecies(term: string) { + const searchConfig: object[] = []; + + const splitTerms = term.split(' '); + + splitTerms.forEach((item) => { + searchConfig.push({ + wildcard: { + english_name: { value: `*${item}*`, boost: 4.0, case_insensitive: true } + } + }); + searchConfig.push({ + wildcard: { unit_name1: { value: `*${item}*`, boost: 3.0, case_insensitive: true } } + }); + searchConfig.push({ + wildcard: { unit_name2: { value: `*${item}*`, boost: 3.0, case_insensitive: true } } + }); + searchConfig.push({ + wildcard: { unit_name3: { value: `*${item}*`, boost: 3.0, case_insensitive: true } } + }); + searchConfig.push({ wildcard: { code: { value: `*${item}*`, boost: 2, case_insensitive: true } } }); + searchConfig.push({ + wildcard: { tty_kingdom: { value: `*${item}*`, boost: 1.0, case_insensitive: true } } + }); + }); + + const response = await this.elasticSearch({ + query: { + bool: { + should: searchConfig + } + } + }); + + return response ? this.sanitizeSpeciesData(response.hits.hits) : []; + } +} diff --git a/api/src/services/user-service.test.ts b/api/src/services/user-service.test.ts new file mode 100644 index 0000000000..39fd18d5bc --- /dev/null +++ b/api/src/services/user-service.test.ts @@ -0,0 +1,630 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import SQL from 'sql-template-strings'; +import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; +import { ApiError } from '../errors/custom-error'; +import { UserObject } from '../models/user'; +import { queries } from '../queries/queries'; +import { getMockDBConnection } from '../__mocks__/db'; +import { UserService } from './user-service'; + +chai.use(sinonChai); + +describe('UserService', () => { + describe('getUserById', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'getUserByIdSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.getUserById(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL select statement'); + } + }); + + it('returns null if the query response has no rows', async function () { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserByIdSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.getUserById(1); + + expect(result).to.be.null; + }); + + it('returns a UserObject for the first row of the response', async function () { + const mockResponseRow = { id: 123 }; + const mockQueryResponse = ({ rows: [mockResponseRow] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserByIdSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.getUserById(1); + + expect(result).to.eql(new UserObject(mockResponseRow)); + }); + }); + + describe('getUserByIdentifier', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'getUserByUserIdentifierSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.getUserByIdentifier('identifier'); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL select statement'); + } + }); + + it('returns null if the query response has no rows', async function () { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserByUserIdentifierSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.getUserByIdentifier('identifier'); + + expect(result).to.be.null; + }); + + it('returns a UserObject for the first row of the response', async function () { + const mockResponseRow = { id: 123 }; + const mockQueryResponse = ({ rows: [mockResponseRow] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserByUserIdentifierSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.getUserByIdentifier('identifier'); + + expect(result).to.eql(new UserObject(mockResponseRow)); + }); + }); + + describe('addSystemUser', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async () => { + const mockDBConnection = getMockDBConnection(); + + const userService = new UserService(mockDBConnection); + + sinon.stub(queries.users, 'addSystemUserSQL').returns(null); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + try { + await userService.addSystemUser(userIdentifier, identitySource); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL insert statement'); + } + }); + + it('should throw an error when response has no rows', async () => { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse }); + + const userService = new UserService(mockDBConnection); + + sinon.stub(queries.users, 'addSystemUserSQL').returns(SQL`valid sql`); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + try { + await userService.addSystemUser(userIdentifier, identitySource); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to insert system user'); + } + }); + + it('should not throw an error on success', async () => { + const mockRowObj = { id: 123 }; + const mockQueryResponse = ({ rows: [mockRowObj] } as unknown) as QueryResult; + const mockQuery = sinon.fake.resolves(mockQueryResponse); + const mockDBConnection = getMockDBConnection({ query: mockQuery }); + + const userService = new UserService(mockDBConnection); + + const addSystemUserSQLStub = sinon.stub(queries.users, 'addSystemUserSQL').returns(SQL`valid sql`); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const result = await userService.addSystemUser(userIdentifier, identitySource); + + expect(result).to.eql(new UserObject(mockRowObj)); + + expect(addSystemUserSQLStub).to.have.been.calledOnce; + expect(mockQuery).to.have.been.calledOnce; + }); + }); + + describe('listSystemUsers', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'getUserListSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.listSystemUsers(); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL select statement'); + } + }); + + it('returns empty array if the query response has no rows', async function () { + const mockQueryResponse = ({ rows: [] } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserListSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.listSystemUsers(); + + expect(result).to.eql([]); + }); + + it('returns a UserObject for each row of the response', async function () { + const mockResponseRow1 = { id: 123 }; + const mockResponseRow2 = { id: 456 }; + const mockResponseRow3 = { id: 789 }; + const mockQueryResponse = ({ + rows: [mockResponseRow1, mockResponseRow2, mockResponseRow3] + } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'getUserListSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.listSystemUsers(); + + expect(result).to.eql([ + new UserObject(mockResponseRow1), + new UserObject(mockResponseRow2), + new UserObject(mockResponseRow3) + ]); + }); + }); + + describe('ensureSystemUser', () => { + afterEach(() => { + sinon.restore(); + }); + + it('throws an error if it fails to get the current system user id', async () => { + const mockDBConnection = getMockDBConnection({ systemUserId: () => null }); + + const existingSystemUser = null; + const getUserByIdentifierStub = sinon + .stub(UserService.prototype, 'getUserByIdentifier') + .resolves(existingSystemUser); + + const addSystemUserStub = sinon.stub(UserService.prototype, 'addSystemUser'); + const activateSystemUserStub = sinon.stub(UserService.prototype, 'activateSystemUser'); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const userService = new UserService(mockDBConnection); + + try { + await userService.ensureSystemUser(userIdentifier, identitySource); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to identify system user ID'); + } + + expect(getUserByIdentifierStub).to.have.been.calledOnce; + expect(addSystemUserStub).not.to.have.been.called; + expect(activateSystemUserStub).not.to.have.been.called; + }); + + it('adds a new system user if one does not already exist', async () => { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const existingSystemUser = null; + const getUserByIdentifierStub = sinon + .stub(UserService.prototype, 'getUserByIdentifier') + .resolves(existingSystemUser); + + const addedSystemUser = new UserObject({ system_user_id: 2, record_end_date: null }); + const addSystemUserStub = sinon.stub(UserService.prototype, 'addSystemUser').resolves(addedSystemUser); + + const activateSystemUserStub = sinon.stub(UserService.prototype, 'activateSystemUser'); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const userService = new UserService(mockDBConnection); + + const result = await userService.ensureSystemUser(userIdentifier, identitySource); + + expect(result.id).to.equal(2); + expect(result.record_end_date).to.equal(null); + + expect(getUserByIdentifierStub).to.have.been.calledOnce; + expect(addSystemUserStub).to.have.been.calledOnce; + expect(activateSystemUserStub).not.to.have.been.called; + }); + + it('gets an existing system user that is already activate', async () => { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const existingInactiveSystemUser = new UserObject({ + system_user_id: 2, + user_identifier: SYSTEM_IDENTITY_SOURCE.IDIR, + record_end_date: null, + role_ids: [1], + role_names: ['Editor'] + }); + const getUserByIdentifierStub = sinon + .stub(UserService.prototype, 'getUserByIdentifier') + .resolves(existingInactiveSystemUser); + + const addSystemUserStub = sinon.stub(UserService.prototype, 'addSystemUser'); + + const activateSystemUserStub = sinon.stub(UserService.prototype, 'activateSystemUser'); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const userService = new UserService(mockDBConnection); + + const result = await userService.ensureSystemUser(userIdentifier, identitySource); + + expect(result.id).to.equal(2); + expect(result.record_end_date).to.equal(null); + + expect(getUserByIdentifierStub).to.have.been.calledOnce; + expect(addSystemUserStub).not.to.have.been.called; + expect(activateSystemUserStub).not.to.have.been.called; + }); + + it('throws an error if it fails to get the newly activated user', async () => { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const existingSystemUser = new UserObject({ + system_user_id: 2, + user_identifier: SYSTEM_IDENTITY_SOURCE.IDIR, + record_end_date: '2021-11-22', + role_ids: [1], + role_names: ['Editor'] + }); + const getUserByIdentifierStub = sinon + .stub(UserService.prototype, 'getUserByIdentifier') + .resolves(existingSystemUser); + + const addSystemUserStub = sinon.stub(UserService.prototype, 'addSystemUser'); + + const activateSystemUserStub = sinon.stub(UserService.prototype, 'activateSystemUser'); + + const activatedSystemUser = null; + const getUserByIdStub = sinon.stub(UserService.prototype, 'getUserById').resolves(activatedSystemUser); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const userService = new UserService(mockDBConnection); + + try { + await userService.ensureSystemUser(userIdentifier, identitySource); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to ensure system user'); + } + + expect(getUserByIdentifierStub).to.have.been.calledOnce; + expect(addSystemUserStub).not.to.have.been.called; + expect(activateSystemUserStub).to.have.been.calledOnce; + expect(getUserByIdStub).to.have.been.calledOnce; + }); + + it('gets an existing system user that is not already active and re-activates it', async () => { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const existingSystemUser = new UserObject({ + system_user_id: 2, + user_identifier: SYSTEM_IDENTITY_SOURCE.IDIR, + record_end_date: '2021-11-22', + role_ids: [1], + role_names: ['Editor'] + }); + const getUserByIdentifierStub = sinon + .stub(UserService.prototype, 'getUserByIdentifier') + .resolves(existingSystemUser); + + const addSystemUserStub = sinon.stub(UserService.prototype, 'addSystemUser'); + + const activateSystemUserStub = sinon.stub(UserService.prototype, 'activateSystemUser'); + + const activatedSystemUser = new UserObject({ + system_user_id: 2, + user_identifier: SYSTEM_IDENTITY_SOURCE.IDIR, + record_end_date: null, + role_ids: [1], + role_names: ['Editor'] + }); + const getUserByIdStub = sinon.stub(UserService.prototype, 'getUserById').resolves(activatedSystemUser); + + const userIdentifier = 'username'; + const identitySource = SYSTEM_IDENTITY_SOURCE.IDIR; + + const userService = new UserService(mockDBConnection); + + const result = await userService.ensureSystemUser(userIdentifier, identitySource); + + expect(result.id).to.equal(2); + expect(result.record_end_date).to.equal(null); + + expect(getUserByIdentifierStub).to.have.been.calledOnce; + expect(addSystemUserStub).not.to.have.been.called; + expect(activateSystemUserStub).to.have.been.calledOnce; + expect(getUserByIdStub).to.have.been.calledOnce; + }); + }); + + describe('activateSystemUser', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'activateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.activateSystemUser(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL update statement'); + } + }); + + it('throws an error if the query response has no rowCount', async function () { + const mockQueryResponse = ({ rowCount: 0 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'activateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.activateSystemUser(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to activate system user'); + } + }); + + it('returns nothing on success', async function () { + const mockQueryResponse = ({ rowCount: 1 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'activateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.activateSystemUser(1); + + expect(result).to.be.undefined; + }); + }); + + describe('deactivateSystemUser', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'deactivateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.deactivateSystemUser(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL update statement'); + } + }); + + it('throws an error if the query response has no rowCount', async function () { + const mockQueryResponse = ({ rowCount: 0 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'deactivateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.deactivateSystemUser(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to deactivate system user'); + } + }); + + it('returns nothing on success', async function () { + const mockQueryResponse = ({ rowCount: 1 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'deactivateSystemUserSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.deactivateSystemUser(1); + + expect(result).to.be.undefined; + }); + }); + + describe('deleteUserSystemRoles', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'deleteAllSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.deleteUserSystemRoles(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL delete statement'); + } + }); + + it('throws an error if the query response has no rowCount', async function () { + const mockQueryResponse = ({ rowCount: 0 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'deleteAllSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.deleteUserSystemRoles(1); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to delete user system roles'); + } + }); + + it('returns nothing on success', async function () { + const mockQueryResponse = ({ rowCount: 1 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'deleteAllSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.deleteUserSystemRoles(1); + + expect(result).to.be.undefined; + }); + }); + + describe('addUserSystemRoles', function () { + afterEach(() => { + sinon.restore(); + }); + + it('should throw an error when no sql statement produced', async function () { + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1 }); + + const mockUsersByIdSQLResponse = null; + sinon.stub(queries.users, 'postSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.addUserSystemRoles(1, [1]); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to build SQL insert statement'); + } + }); + + it('throws an error if the query response has no rowCount', async function () { + const mockQueryResponse = ({ rowCount: 0 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'postSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + try { + await userService.addUserSystemRoles(1, [1]); + expect.fail(); + } catch (actualError) { + expect((actualError as ApiError).message).to.equal('Failed to insert user system roles'); + } + }); + + it('returns nothing on success', async function () { + const mockQueryResponse = ({ rowCount: 1 } as unknown) as QueryResult; + const mockDBConnection = getMockDBConnection({ systemUserId: () => 1, query: async () => mockQueryResponse }); + + const mockUsersByIdSQLResponse = SQL`Test SQL Statement`; + sinon.stub(queries.users, 'postSystemRolesSQL').returns(mockUsersByIdSQLResponse); + + const userService = new UserService(mockDBConnection); + + const result = await userService.addUserSystemRoles(1, [1]); + + expect(result).to.be.undefined; + }); + }); +}); diff --git a/api/src/services/user-service.ts b/api/src/services/user-service.ts new file mode 100644 index 0000000000..1967e1098c --- /dev/null +++ b/api/src/services/user-service.ts @@ -0,0 +1,228 @@ +import { ApiBuildSQLError, ApiExecuteSQLError } from '../errors/custom-error'; +import { UserObject } from '../models/user'; +import { queries } from '../queries/queries'; +import { DBService } from './service'; + +export type ListSystemUsers = { + id: number; + user_identifier: string; + record_end_date: string; + role_ids: number[]; + role_names: string[]; +}; + +export class UserService extends DBService { + /** + * Fetch a single system user by their ID. + * + * @param {number} systemUserId + * @return {*} {(Promise)} + * @memberof UserService + */ + async getUserById(systemUserId: number): Promise { + const sqlStatement = queries.users.getUserByIdSQL(systemUserId); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + return (response?.rows?.[0] && new UserObject(response.rows[0])) || null; + } + + /** + * Get an existing system user. + * + * @param {string} userIdentifier + * @return {*} {(Promise)} + * @memberof UserService + */ + async getUserByIdentifier(userIdentifier: string): Promise { + const sqlStatement = queries.users.getUserByUserIdentifierSQL(userIdentifier); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL select statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + return (response?.rows?.[0] && new UserObject(response.rows[0])) || null; + } + + /** + * Adds a new system user. + * + * Note: Will fail if the system user already exists. + * + * @param {string} userIdentifier + * @param {string} identitySource + * @return {*} {Promise} + * @memberof UserService + */ + async addSystemUser(userIdentifier: string, identitySource: string): Promise { + const addSystemUserSQLStatement = queries.users.addSystemUserSQL(userIdentifier, identitySource); + + if (!addSystemUserSQLStatement) { + throw new ApiBuildSQLError('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(addSystemUserSQLStatement.text, addSystemUserSQLStatement.values); + + const userObject = (response?.rows?.[0] && new UserObject(response.rows[0])) || null; + + if (!userObject) { + throw new ApiExecuteSQLError('Failed to insert system user'); + } + + return userObject; + } + + /** + * Get a list of all system users. + * + * @return {*} {Promise} + * @memberof UserService + */ + async listSystemUsers(): Promise { + const getUserListSQLStatement = queries.users.getUserListSQL(); + + if (!getUserListSQLStatement) { + throw new ApiBuildSQLError('Failed to build SQL select statement'); + } + + const getUserListResponse = await this.connection.query( + getUserListSQLStatement.text, + getUserListSQLStatement.values + ); + + return getUserListResponse.rows.map((row) => new UserObject(row)); + } + + /** + * Gets a system user, adding them if they do not already exist, or activating them if they had been deactivated (soft + * deleted). + * + * @param {string} userIdentifier + * @param {string} identitySource + * @param {IDBConnection} connection + * @return {*} {Promise} + * @memberof UserService + */ + async ensureSystemUser(userIdentifier: string, identitySource: string): Promise { + // Check if the user exists in SIMS + let userObject = await this.getUserByIdentifier(userIdentifier); + + if (!userObject) { + // Id of the current authenticated user + const systemUserId = this.connection.systemUserId(); + + if (!systemUserId) { + throw new ApiExecuteSQLError('Failed to identify system user ID'); + } + + // Found no existing user, add them + userObject = await this.addSystemUser(userIdentifier, identitySource); + } + + if (!userObject.record_end_date) { + // system user is already active + return userObject; + } + + // system user is not active, re-activate them + await this.activateSystemUser(userObject.id); + + // get the newly activated user + userObject = await this.getUserById(userObject.id); + + if (!userObject) { + throw new ApiExecuteSQLError('Failed to ensure system user'); + } + + return userObject; + } + + /** + * Activates an existing system user that had been deactivated (soft deleted). + * + * @param {number} systemUserId + * @return {*} {(Promise)} + * @memberof UserService + */ + async activateSystemUser(systemUserId: number) { + const sqlStatement = queries.users.activateSystemUserSQL(systemUserId); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL update statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to activate system user'); + } + } + + /** + * Deactivates an existing system user (soft delete). + * + * @param {number} systemUserId + * @return {*} {(Promise)} + * @memberof UserService + */ + async deactivateSystemUser(systemUserId: number) { + const sqlStatement = queries.users.deactivateSystemUserSQL(systemUserId); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL update statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to deactivate system user'); + } + } + + /** + * Delete all system roles for the user. + * + * @param {number} systemUserId + * @memberof UserService + */ + async deleteUserSystemRoles(systemUserId: number) { + const sqlStatement = queries.users.deleteAllSystemRolesSQL(systemUserId); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL delete statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to delete user system roles'); + } + } + + /** + * Adds the specified roleIds to the user. + * + * @param {number} systemUserId + * @param {number[]} roleIds + * @memberof UserService + */ + async addUserSystemRoles(systemUserId: number, roleIds: number[]) { + const sqlStatement = queries.users.postSystemRolesSQL(systemUserId, roleIds); + + if (!sqlStatement) { + throw new ApiBuildSQLError('Failed to build SQL insert statement'); + } + + const response = await this.connection.query(sqlStatement.text, sqlStatement.values); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to insert user system roles'); + } + } +} diff --git a/api/src/utils/code-utils.ts b/api/src/utils/code-utils.ts deleted file mode 100644 index d82d88bdef..0000000000 --- a/api/src/utils/code-utils.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { IDBConnection } from '../database/db'; -import { - getFirstNationsSQL, - getFundingSourceSQL, - getInvestmentActionCategorySQL, - getManagementActionTypeSQL, - getIUCNConservationActionLevel1ClassificationSQL, - getIUCNConservationActionLevel2SubclassificationSQL, - getIUCNConservationActionLevel3SubclassificationSQL, - getActivitySQL, - getProjectTypeSQL, - getSystemRolesSQL, - getProprietorTypeSQL, - getAdministrativeActivityStatusTypeSQL, - getTaxonsSQL, - getCommonSurveyMethodologiesSQL -} from '../queries/codes/code-queries'; -import { getLogger } from '../utils/logger'; -import { coordinator_agency, region, regional_offices } from '../constants/codes'; - -const defaultLog = getLogger('queries/code-queries'); - -export interface IAllCodeSets { - management_action_type: object; - first_nations: object; - funding_source: object; - investment_action_category: object; - activity: object; - project_type: object; - coordinator_agency: object; - region: object; - species: object; - proprietor_type: object; - iucn_conservation_action_level_1_classification: object; - iucn_conservation_action_level_2_subclassification: object; - iucn_conservation_action_level_3_subclassification: object; - system_roles: object; - regional_offices: object; - administrative_activity_status_type: object; - common_survey_methodologies: object; -} - -/** - * Function that fetches all code sets. - * - * @param {PoolClient} connection - * @returns {IAllCodeSets} an object containing all code sets - */ -export async function getAllCodeSets(connection: IDBConnection): Promise { - defaultLog.debug({ message: 'getAllCodeSets' }); - - await connection.open(); - - const [ - management_action_type, - first_nations, - funding_source, - investment_action_category, - activity, - iucn_conservation_action_level_1_classification, - iucn_conservation_action_level_2_subclassification, - iucn_conservation_action_level_3_subclassification, - proprietor_type, - project_type, - system_roles, - administrative_activity_status_type, - species, - common_survey_methodologies - ] = await Promise.all([ - await connection.query(getManagementActionTypeSQL().text), - await connection.query(getFirstNationsSQL().text), - await connection.query(getFundingSourceSQL().text), - await connection.query(getInvestmentActionCategorySQL().text), - await connection.query(getActivitySQL().text), - await connection.query(getIUCNConservationActionLevel1ClassificationSQL().text), - await connection.query(getIUCNConservationActionLevel2SubclassificationSQL().text), - await connection.query(getIUCNConservationActionLevel3SubclassificationSQL().text), - await connection.query(getProprietorTypeSQL().text), - await connection.query(getProjectTypeSQL().text), - await connection.query(getSystemRolesSQL().text), - await connection.query(getAdministrativeActivityStatusTypeSQL().text), - await connection.query(getTaxonsSQL().text), - await connection.query(getCommonSurveyMethodologiesSQL().text) - ]); - - await connection.commit(); - - connection.release(); - - return { - management_action_type: (management_action_type && management_action_type.rows) || [], - first_nations: (first_nations && first_nations.rows) || [], - funding_source: (funding_source && funding_source.rows) || [], - investment_action_category: (investment_action_category && investment_action_category.rows) || [], - activity: (activity && activity.rows) || [], - iucn_conservation_action_level_1_classification: - (iucn_conservation_action_level_1_classification && iucn_conservation_action_level_1_classification.rows) || [], - iucn_conservation_action_level_2_subclassification: - (iucn_conservation_action_level_2_subclassification && iucn_conservation_action_level_2_subclassification.rows) || - [], - iucn_conservation_action_level_3_subclassification: - (iucn_conservation_action_level_3_subclassification && iucn_conservation_action_level_3_subclassification.rows) || - [], - proprietor_type: (proprietor_type && proprietor_type.rows) || [], - project_type: (project_type && project_type.rows) || [], - system_roles: (system_roles && system_roles.rows) || [], - administrative_activity_status_type: - (administrative_activity_status_type && administrative_activity_status_type.rows) || [], - species: (species && species.rows) || [], - common_survey_methodologies: (common_survey_methodologies && common_survey_methodologies.rows) || [], - // TODO Temporarily hard coded list of code values below - coordinator_agency, - region, - regional_offices - }; -} diff --git a/api/src/utils/db-constant-utils.ts b/api/src/utils/db-constant-utils.ts new file mode 100644 index 0000000000..2c805586d0 --- /dev/null +++ b/api/src/utils/db-constant-utils.ts @@ -0,0 +1,25 @@ +import { IDBConnection } from '../database/db'; +import { HTTP400 } from '../errors/custom-error'; +import { queries } from '../queries/queries'; + +/** + * Get db character metadata constants. + * + * @param {string} constantName + * @param {IDBConnection} connection + * @return {*} {Promise} + */ +export const getDbCharacterSystemMetaDataConstant = async ( + constantName: string, + connection: IDBConnection +): Promise => { + const sqlStatement = queries.codes.getDbCharacterSystemMetaDataConstantSQL(constantName); + + if (!sqlStatement) { + throw new HTTP400('Failed to build SQL update statement'); + } + + const response = await connection.query(sqlStatement.text, sqlStatement.values); + + return response.rows?.[0].constant || null; +}; diff --git a/api/src/utils/file-utils.ts b/api/src/utils/file-utils.ts index b95dced80f..96a0d39bb9 100644 --- a/api/src/utils/file-utils.ts +++ b/api/src/utils/file-utils.ts @@ -1,7 +1,7 @@ import AWS from 'aws-sdk'; import { DeleteObjectOutput, GetObjectOutput, ManagedUpload, Metadata } from 'aws-sdk/clients/s3'; -import { S3_ROLE } from '../constants/roles'; import clamd from 'clamdjs'; +import { S3_ROLE } from '../constants/roles'; const scanner = process.env.ENABLE_FILE_VIRUS_SCAN === 'true' diff --git a/api/src/utils/keycloak-utils.test.ts b/api/src/utils/keycloak-utils.test.ts index e2f5b2b40f..f4423fa297 100644 --- a/api/src/utils/keycloak-utils.test.ts +++ b/api/src/utils/keycloak-utils.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; -import { getUserIdentifier, getUserIdentitySource } from './keycloak-utils'; +import { convertUserIdentitySource, getUserIdentifier, getUserIdentitySource } from './keycloak-utils'; describe('getUserIdentifier', () => { it('returns null response when null keycloakToken provided', () => { @@ -36,45 +36,137 @@ describe('getUserIdentifier', () => { }); describe('getUserIdentitySource', () => { - it('returns non null response when null keycloakToken provided', () => { + it('returns null response when null keycloakToken provided', () => { const response = getUserIdentitySource((null as unknown) as object); - expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + expect(response).to.equal(null); }); - it('returns non null response when valid keycloakToken provided with no preferred_username', () => { + it('returns null response when valid keycloakToken provided with no preferred_username', () => { const response = getUserIdentitySource({}); - expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + expect(response).to.equal(null); }); - it('returns non null response when valid keycloakToken provided with null preferred_username', () => { + it('returns null response when valid keycloakToken provided with null preferred_username', () => { const response = getUserIdentitySource({ preferred_username: null }); - expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + expect(response).to.equal(null); }); - it('returns non null response when valid keycloakToken provided with no source', () => { + it('returns null response when valid keycloakToken provided with no source', () => { const response = getUserIdentitySource({ preferred_username: 'username' }); - expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + expect(response).to.equal(null); }); - it('returns non null response when valid keycloakToken provided with idir source', () => { + it('returns non null response when valid keycloakToken provided with lowercase idir source', () => { const response = getUserIdentitySource({ preferred_username: 'username@idir' }); expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.IDIR); }); - it('returns non null response when valid keycloakToken provided with bceid source', () => { + it('returns non null response when valid keycloakToken provided with lowercase bceid source', () => { const response = getUserIdentitySource({ preferred_username: 'username@bceid' }); expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); }); - it('returns non null response when valid keycloakToken provided with database source', () => { + it('returns non null response when valid keycloakToken provided with lowercase bceid basic and business source', () => { + const response = getUserIdentitySource({ preferred_username: 'username@bceid-basic-and-business' }); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when valid keycloakToken provided with lowercase database source', () => { const response = getUserIdentitySource({ preferred_username: 'username@database' }); expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); }); + + it('returns non null response when valid keycloakToken provided with uppercase idir source', () => { + const response = getUserIdentitySource({ preferred_username: 'username@IDIR' }); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.IDIR); + }); + + it('returns non null response when valid keycloakToken provided with uppercase bceid source', () => { + const response = getUserIdentitySource({ preferred_username: 'username@BCEID' }); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when valid keycloakToken provided with uppercase bceid basic and business source', () => { + const response = getUserIdentitySource({ preferred_username: 'username@BCEID-BASIC-AND-BUSINESS' }); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when valid keycloakToken provided with uppercase database source', () => { + const response = getUserIdentitySource({ preferred_username: 'username@DATABASE' }); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + }); + + describe('convertUserIdentitySource', () => { + it('returns null response when null identity source provided', () => { + const response = convertUserIdentitySource((null as unknown) as string); + + expect(response).to.equal(null); + }); + + it('returns null response when empty identity source provided', () => { + const response = convertUserIdentitySource(''); + + expect(response).to.equal(null); + }); + + it('returns non null response when lowercase idir source provided', () => { + const response = convertUserIdentitySource('idir'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.IDIR); + }); + + it('returns non null response when lowercase bceid source provided', () => { + const response = convertUserIdentitySource('bceid'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when lowercase bceid basic and business source provided', () => { + const response = convertUserIdentitySource('bceid-basic-and-business'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when lowercase database source provided', () => { + const response = convertUserIdentitySource('database'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + }); + + it('returns non null response when uppercase idir source provided', () => { + const response = convertUserIdentitySource('IDIR'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.IDIR); + }); + + it('returns non null response when uppercase bceid source provided', () => { + const response = convertUserIdentitySource('BCEID'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when uppercase bceid basic and business source provided', () => { + const response = convertUserIdentitySource('BCEID-BASIC-AND-BUSINESS'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.BCEID); + }); + + it('returns non null response when uppercase database source provided', () => { + const response = convertUserIdentitySource('DATABASE'); + + expect(response).to.equal(SYSTEM_IDENTITY_SOURCE.DATABASE); + }); + }); }); diff --git a/api/src/utils/keycloak-utils.ts b/api/src/utils/keycloak-utils.ts index d0356f2de5..e32e7ceda6 100644 --- a/api/src/utils/keycloak-utils.ts +++ b/api/src/utils/keycloak-utils.ts @@ -1,4 +1,5 @@ import { SYSTEM_IDENTITY_SOURCE } from '../constants/database'; +import { EXTERNAL_BCEID_IDENTITY_SOURCES, EXTERNAL_IDIR_IDENTITY_SOURCES } from '../constants/keycloak'; /** * Parses out the preferred_username name from the token. @@ -20,23 +21,37 @@ export const getUserIdentifier = (keycloakToken: object): string | null => { * Parses out the preferred_username identity source (idir, bceid, etc) from the token. * * @param {object} keycloakToken - * @return {*} {SYSTEM_IDENTITY_SOURCE} + * @return {*} {(SYSTEM_IDENTITY_SOURCE | null)} */ -export const getUserIdentitySource = (keycloakToken: object): SYSTEM_IDENTITY_SOURCE => { - const userIdentitySource = keycloakToken?.['preferred_username']?.split('@')?.[1]; +export const getUserIdentitySource = (keycloakToken: object): SYSTEM_IDENTITY_SOURCE | null => { + const userIdentitySource = keycloakToken?.['preferred_username']?.split('@')?.[1]?.toUpperCase(); - if (userIdentitySource?.toUpperCase() === SYSTEM_IDENTITY_SOURCE.BCEID) { + return convertUserIdentitySource(userIdentitySource); +}; + +/** + * Converts an identity source string to a matching one supported by the database. + * + * Why? Some identity sources ave multiple variations of their source string, which the get translated to a single + * variation so that the SIMS application doesn't have to account for every variation in its logic. + * + * @param {object} keycloakToken + * @return {*} {(SYSTEM_IDENTITY_SOURCE | null)} + */ +export const convertUserIdentitySource = (identitySource: string): SYSTEM_IDENTITY_SOURCE | null => { + const uppercaseIdentitySource = identitySource?.toUpperCase(); + + if (EXTERNAL_BCEID_IDENTITY_SOURCES.includes(uppercaseIdentitySource)) { return SYSTEM_IDENTITY_SOURCE.BCEID; } - if (userIdentitySource?.toUpperCase() === SYSTEM_IDENTITY_SOURCE.IDIR) { + if (EXTERNAL_IDIR_IDENTITY_SOURCES.includes(uppercaseIdentitySource)) { return SYSTEM_IDENTITY_SOURCE.IDIR; } - if (userIdentitySource?.toUpperCase() === SYSTEM_IDENTITY_SOURCE.DATABASE) { + if (uppercaseIdentitySource === SYSTEM_IDENTITY_SOURCE.DATABASE) { return SYSTEM_IDENTITY_SOURCE.DATABASE; } - // Covers users created directly in keycloak, that wouldn't have identity source - return SYSTEM_IDENTITY_SOURCE.DATABASE; + return null; }; diff --git a/api/src/utils/logger.test.ts b/api/src/utils/logger.test.ts index 6540415975..c5f0c763d4 100644 --- a/api/src/utils/logger.test.ts +++ b/api/src/utils/logger.test.ts @@ -1,6 +1,14 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { isObject, isObjectWithkeys, prettyPrint, getPrintfFunction, ILoggerMessage } from './logger'; +import { ApiError, ApiErrorType, HTTP500 } from '../errors/custom-error'; +import { + getPrintfFunction, + ILoggerMessage, + isObject, + isObjectWithkeys, + prettyPrint, + prettyPrintUnknown +} from './logger'; describe('isObject', () => { it('identifies if object is valid when undefined', () => { @@ -116,6 +124,49 @@ describe('prettyPrint', () => { }); }); +describe('prettyPrintUnknown', () => { + it('returns empty string if input is null', () => { + expect(prettyPrintUnknown('')).to.equal(''); + expect(prettyPrintUnknown(null)).to.equal(''); + expect(prettyPrintUnknown(undefined)).to.equal(''); + }); + + it('returns formatted HTTPError', () => { + const testError = new HTTP500('a 500 error'); + expect(prettyPrintUnknown(testError)).to.equal(`500 ${testError.stack}`); + }); + + it('returns formatted ApiError', () => { + const testError = new ApiError(ApiErrorType.GENERAL, 'an unknown error'); + expect(prettyPrintUnknown(testError)).to.equal(`${testError.stack}`); + }); + + it('returns formatted Error', () => { + const testError = new Error('an error'); + expect(prettyPrintUnknown(testError)).to.equal(`${testError.stack}`); + }); + + it('returns formatted object', () => { + expect(prettyPrintUnknown({ param1: 1, param2: 2 })).to.equal('{\n "param1": 1,\n "param2": 2\n}'); + }); + + it('returns formatted array', () => { + expect(prettyPrintUnknown([1, 2, 3])).to.equal('[\n 1,\n 2,\n 3\n]'); + }); + + it('returns original value if it is a string', () => { + expect(prettyPrintUnknown('a string')).to.equal('a string'); + }); + + it('returns original value if it is a number', () => { + expect(prettyPrintUnknown(1234)).to.equal(1234); + }); + + it('returns empty string if input is an empty object', () => { + expect(prettyPrintUnknown({})).to.equal(''); + }); +}); + describe('getPrintfFunction', () => { let printFunction: (args: ILoggerMessage) => string; @@ -124,29 +175,33 @@ describe('getPrintfFunction', () => { }); it('returns template string without additional objects', () => { + const testError = new Error('an error'); + const result = printFunction({ timestamp: '2021-10-20', level: 'info', label: 'label', message: 'message', - error: new Error('an error') + error: testError }); - expect(result).to.equal('[2021-10-20] (info) (logLabel): label - message \nError: an error '); + expect(result).to.equal(`[2021-10-20] (info) (logLabel): label - message\n${testError.stack}`); }); it('returns template string with additional objects', () => { + const testError = new Error('an error'); + const result = printFunction({ timestamp: '2021-10-20', level: 'info', label: 'label', message: 'message', - error: new Error('an error'), + error: testError, additionalObj: { a: 1 } }); expect(result).to.equal( - '[2021-10-20] (info) (logLabel): label - message \nError: an error \n{\n "additionalObj": {\n "a": 1\n }\n}' + `[2021-10-20] (info) (logLabel): label - message\n${testError.stack}\n{\n "additionalObj": {\n "a": 1\n }\n}` ); }); }); diff --git a/api/src/utils/logger.ts b/api/src/utils/logger.ts index b3f1710167..c295bce782 100644 --- a/api/src/utils/logger.ts +++ b/api/src/utils/logger.ts @@ -1,4 +1,5 @@ import winston from 'winston'; +import { ApiError, HTTPError } from '../errors/custom-error'; /** * Logger input. @@ -43,6 +44,41 @@ export const prettyPrint = (item: any): string => { return JSON.stringify(item, undefined, 2); }; +/** + * Pretty stringify an item of unknown type. + * + * @param {*} item + * @return {*} {string} + */ +export const prettyPrintUnknown = (item: any): string => { + if (!item) { + return ''; + } + + if (item instanceof HTTPError) { + return `${item.status} ${item.stack}` + ((item.errors?.length && `\n${prettyPrintUnknown(item.errors)}`) || ''); + } + + if (item instanceof ApiError) { + return `${item.stack}` + ((item.errors?.length && `\n${prettyPrintUnknown(item.errors)}`) || ''); + } + + if (item instanceof Error) { + return `${item.stack}`; + } + + if (isObjectWithkeys(item)) { + return prettyPrint(item); + } + + if (isObject(item)) { + // is an object, but has no real properties, so print nothing + return ''; + } + + return item; +}; + /** * Returns a printf function. * @@ -53,13 +89,17 @@ export const getPrintfFunction = (logLabel: string): ((args: ILoggerMessage) => return ({ timestamp, level, label, message, error, ...other }: ILoggerMessage) => { const optionalLabel = (label && ` ${label} -`) || ''; - const logMessage = (message && ((isObject(message) && `${prettyPrint(message)}`) || message)) || ''; + const logMessage = (message && prettyPrintUnknown(message)) || ''; - const optionalError = (error && ((isObjectWithkeys(error) && `\n${prettyPrint(error)}`) || `\n${error}`)) || ''; + const optionalError = (error && prettyPrintUnknown(error)) || ''; - const optionalOther = (other && isObjectWithkeys(other) && `\n${JSON.stringify(other, undefined, 2)}`) || ''; + const optionalOther = (other && prettyPrintUnknown(other)) || ''; - return `[${timestamp}] (${level}) (${logLabel}):${optionalLabel} ${logMessage} ${optionalError} ${optionalOther}`; + return ( + `[${timestamp}] (${level}) (${logLabel}):${optionalLabel} ${logMessage}` + + (optionalError && `\n${optionalError}`) + + (optionalOther && `\n${optionalOther}`) + ); }; }; @@ -107,7 +147,7 @@ export const getPrintfFunction = (logLabel: string): ((args: ILoggerMessage) => * ...etc * * Valid `LOG_LEVEL` values (from least logging to most logging) (default: info): - * error, warn, info, debug + * silent, error, warn, info, debug, silly * * @param {string} logLabel common label for the instance of the logger. * @returns @@ -127,3 +167,18 @@ export const getLogger = function (logLabel: string) { ] }); }; + +export const WinstonLogLevels = ['silent', 'error', 'warn', 'info', 'debug', 'silly'] as const; + +export type WinstonLogLevel = typeof WinstonLogLevels[number]; + +/** + * Set the winston logger log level. + * + * @param {WinstonLogLevel} logLevel + */ +export const setLogLevel = (logLevel: WinstonLogLevel) => { + winston.loggers.loggers.forEach((logger) => { + logger.transports[0].level = logLevel; + }); +}; diff --git a/api/src/utils/media/csv/validation/csv-header-validator.test.ts b/api/src/utils/media/csv/validation/csv-header-validator.test.ts index 08f061aef2..9ba17182de 100644 --- a/api/src/utils/media/csv/validation/csv-header-validator.test.ts +++ b/api/src/utils/media/csv/validation/csv-header-validator.test.ts @@ -5,8 +5,8 @@ import { CSVWorksheet } from '../csv-file'; import { getDuplicateHeadersValidator, getValidHeadersValidator, - hasRequiredHeadersValidator, - hasRecommendedHeadersValidator + hasRecommendedHeadersValidator, + hasRequiredHeadersValidator } from './csv-header-validator'; describe('getDuplicateHeadersValidator', () => { diff --git a/api/src/utils/media/csv/validation/csv-row-validator.test.ts b/api/src/utils/media/csv/validation/csv-row-validator.test.ts index d4e135206e..cf4976d857 100644 --- a/api/src/utils/media/csv/validation/csv-row-validator.test.ts +++ b/api/src/utils/media/csv/validation/csv-row-validator.test.ts @@ -4,10 +4,10 @@ import xlsx from 'xlsx'; import { CSVWorksheet } from '../csv-file'; import { getCodeValueFieldsValidator, - getRequiredFieldsValidator, - getValidRangeFieldsValidator, getNumericFieldsValidator, - getValidFormatFieldsValidator + getRequiredFieldsValidator, + getValidFormatFieldsValidator, + getValidRangeFieldsValidator } from './csv-row-validator'; describe('getRequiredFieldsValidator', () => { diff --git a/api/src/utils/media/media-file.ts b/api/src/utils/media/media-file.ts index 9f9033d32a..64e6b96c14 100644 --- a/api/src/utils/media/media-file.ts +++ b/api/src/utils/media/media-file.ts @@ -33,7 +33,14 @@ export class MediaFile implements IMediaFile { * @memberof MediaFile */ get name(): string { - return this.fileName.split('.')[0]; + const lastPeriodindex = this.fileName.lastIndexOf('.'); + + if (lastPeriodindex >= 0) { + // strip out the file extension, if it exists + return this.fileName.substring(0, lastPeriodindex); + } else { + return this.fileName; + } } /** @@ -84,7 +91,14 @@ export class ArchiveFile implements IMediaFile { * @memberof ArchiveFile */ get name(): string { - return this.fileName.split('.')[0]; + const lastPeriodindex = this.fileName.lastIndexOf('.'); + + if (lastPeriodindex >= 0) { + // strip out the file extension, if it exists + return this.fileName.substring(0, lastPeriodindex); + } else { + return this.fileName; + } } /** diff --git a/api/src/utils/media/validation/validation-schema-parser.ts b/api/src/utils/media/validation/validation-schema-parser.ts index 1776fca4f6..472d22e6c8 100644 --- a/api/src/utils/media/validation/validation-schema-parser.ts +++ b/api/src/utils/media/validation/validation-schema-parser.ts @@ -8,10 +8,10 @@ import { } from '../csv/validation/csv-header-validator'; import { getCodeValueFieldsValidator, + getNumericFieldsValidator, getRequiredFieldsValidator, getValidFormatFieldsValidator, - getValidRangeFieldsValidator, - getNumericFieldsValidator + getValidRangeFieldsValidator } from '../csv/validation/csv-row-validator'; import { DWCArchiveValidator } from '../dwc/dwc-archive-file'; import { XLSXCSVValidator } from '../xlsx/xlsx-file'; diff --git a/api/src/utils/media/xlsx/transformation/transformation-schema-parser.ts b/api/src/utils/media/xlsx/transformation/transformation-schema-parser.ts index 37fb451156..ac670a34da 100644 --- a/api/src/utils/media/xlsx/transformation/transformation-schema-parser.ts +++ b/api/src/utils/media/xlsx/transformation/transformation-schema-parser.ts @@ -21,6 +21,7 @@ export type TransformationFieldsSchema = { export type Condition = { if: { columns: string[]; + not?: boolean; }; }; @@ -41,7 +42,7 @@ export type TransformSchema = { postTransformations?: PostTransformationRelatopnshipSchema[]; }; -export type ParseColumnSchema = { source: string; target: string }; +export type ParseColumnSchema = { source: { columns?: string[]; value?: any }; target: string }; export type ParseSchema = { fileName: string; @@ -59,8 +60,12 @@ export class TransformationSchemaParser { } } + getAllFlattenSchemas(): FlattenSchema[] | [] { + return jsonpath.query(this.transformationSchema, this.getFlattenJsonPath())?.[0] || []; + } + getFlattenSchemas(fileName: string): FlattenSchema | null { - return jsonpath.query(this.transformationSchema, this.getFlattenJsonPath(fileName))?.[0] || null; + return jsonpath.query(this.transformationSchema, this.getFlattenJsonPathByFileName(fileName))?.[0] || null; } getTransformSchemas(): TransformSchema[] { @@ -71,7 +76,11 @@ export class TransformationSchemaParser { return jsonpath.query(this.transformationSchema, this.getParseJsonPath())?.[0] || []; } - getFlattenJsonPath(fileName: string): string { + getFlattenJsonPath(): string { + return `$.flatten`; + } + + getFlattenJsonPathByFileName(fileName: string): string { return `$.flatten[?(@.fileName == '${fileName}')]`; } diff --git a/api/src/utils/media/xlsx/transformation/xlsx-transformation.ts b/api/src/utils/media/xlsx/transformation/xlsx-transformation.ts index b60840af75..71f9c80497 100644 --- a/api/src/utils/media/xlsx/transformation/xlsx-transformation.ts +++ b/api/src/utils/media/xlsx/transformation/xlsx-transformation.ts @@ -4,6 +4,7 @@ import { CSVWorksheet } from '../../csv/csv-file'; import { XLSXCSV } from '../xlsx-file'; import { Condition, + FlattenSchema, PostTransformationRelatopnshipSchema, TransformationFieldSchema, TransformationSchemaParser, @@ -62,49 +63,55 @@ export class XLSXTransformation { * @memberof XLSXTransformation */ _flattenData(): FlattenedRowPartsBySourceFile[][] { - const rowsBySourceFileArray: FlattenedRowPartsBySourceFile[][] = []; + let rowsBySourceFileArray: FlattenedRowPartsBySourceFile[][] = []; - Object.entries(this.xlsxCsv.workbook.worksheets).forEach(([worksheetName, worksheet]) => { - // Get the file structure schema for the given fileName - const fileStructure = this.transformationSchemaParser.getFlattenSchemas(worksheetName); + // Get all flatten schemas + const flattenSchemas = this.transformationSchemaParser.getAllFlattenSchemas(); - if (!fileStructure) { + // Build an array of [worksheetName, worksheet] based on the order of the flatten schemas. This is necessary + // because the flattening process requires parsing the worksheets in a specific order, as specified by the flatten + // section of the transformation schema. + const orderedWorksheetsByFlattenSchema: [string, CSVWorksheet][] = []; + flattenSchemas.forEach((flattenSchema) => { + const worksheet = this.xlsxCsv.workbook.worksheets[flattenSchema.fileName]; + + if (worksheet) { + orderedWorksheetsByFlattenSchema.push([flattenSchema.fileName, worksheet]); + } + }); + + // Iterate over each worksheet in the ordered array of worksheets + orderedWorksheetsByFlattenSchema.forEach(([worksheetName, worksheet]) => { + // Get the flatten file structure schema for the worksheet, based on the worksheet name + const flattenSchema = this.transformationSchemaParser.getFlattenSchemas(worksheetName); + + if (!flattenSchema) { + // No schema for this worksheet, skip it return; } - // Get all rows, as objects + // Get all worksheet rows as an array of objects const rowObjects = worksheet.getRowObjects(); - if (!fileStructure.parent) { + if (!flattenSchema.parent) { // Handle root records, that have no parent record - - rowObjects.forEach((rowObject, rowIndex) => { - const uniqueId = this._buildMultiColumnID(worksheet, rowIndex, fileStructure.uniqueId); - - const newRecord = { - sourceFile: fileStructure.fileName, - uniqueId: uniqueId, - row: rowObject - }; - - rowsBySourceFileArray.push([newRecord]); - }); + const flattenedRootRecords = this._flattenRootRecords(flattenSchema, worksheet, rowObjects); + rowsBySourceFileArray = rowsBySourceFileArray.concat(flattenedRootRecords); } else { // Handle child records, that have a parent record + const parentFileName = flattenSchema.parent.fileName.toLowerCase(); + const parentUniqueIdColumns = flattenSchema.parent.uniqueId; - const parentFileName = fileStructure.parent.fileName.toLowerCase(); - const parentUniqueIdColumns = fileStructure.parent.uniqueId; - - const fileName = fileStructure.fileName; - const uniqueIdColumns = fileStructure.uniqueId; + const childFileName = flattenSchema.fileName; + const childUniqueIdColumns = flattenSchema.uniqueId; rowObjects.forEach((rowObject, rowIndex) => { const parentUniqueId = this._buildMultiColumnID(worksheet, rowIndex, parentUniqueIdColumns).toLowerCase(); - const uniqueId = this._buildMultiColumnID(worksheet, rowIndex, uniqueIdColumns); + const uniqueId = this._buildMultiColumnID(worksheet, rowIndex, childUniqueIdColumns); const newRecord = { - sourceFile: fileName, + sourceFile: childFileName, uniqueId: uniqueId, row: rowObject }; @@ -143,7 +150,7 @@ export class XLSXTransformation { }; foundRecordToModify = true; - } else if (existingRowFileName === fileName.toLowerCase()) { + } else if (existingRowFileName === childFileName.toLowerCase()) { // This array already contains a record from the same file as `newRecord` and will need to be duplicated recordsToModify[recordsToModifyIndex] = { ...recordsToModify[recordsToModifyIndex], @@ -248,6 +255,28 @@ export class XLSXTransformation { return rowsBySourceFileArray; } + _flattenRootRecords( + flattenSchema: FlattenSchema, + worksheet: CSVWorksheet, + rowObjects: object[] + ): FlattenedRowPartsBySourceFile[][] { + const newRecords: FlattenedRowPartsBySourceFile[][] = []; + + rowObjects.forEach((rowObject, rowIndex) => { + const uniqueId = this._buildMultiColumnID(worksheet, rowIndex, flattenSchema.uniqueId); + + const newRecord = { + sourceFile: flattenSchema.fileName, + uniqueId: uniqueId, + row: rowObject + }; + + newRecords.push([newRecord]); + }); + + return newRecords; + } + _buildMultiColumnID(worksheet: CSVWorksheet, rowIndex: number, columnNames: string[]) { return this._buildMultiColumnValue(worksheet, rowIndex, columnNames, ':'); } @@ -324,7 +353,7 @@ export class XLSXTransformation { return; } - Object.entries(transformation.fields).forEach(([dwcField, config]) => { + Object.entries(transformation.fields).forEach(([fieldName, config]) => { if (!this._isConditionMet(rowObject, config?.condition)) { return; } @@ -336,7 +365,7 @@ export class XLSXTransformation { columnValue = `${columnValue}:${config.unique}-${rowObjectIndex}-${transformationSchemaIndex}`; } - newDWCRowObject[dwcField] = columnValue; + newDWCRowObject[fieldName] = columnValue; }); newDWCRowObjects.push(newDWCRowObject); @@ -477,17 +506,24 @@ export class XLSXTransformation { const columnValueParts = this._getColumnValueParts(rowObject, condition.if.columns); if (!columnValueParts || !columnValueParts.length) { + if (condition.if.not) { + // return true if no condition column values are defined, when condition is inverted + return true; + } + + // return false if no condition column values are defined return false; } - for (const columnValuePart in columnValueParts) { - if (columnValuePart === undefined || columnValuePart === null || columnValuePart === '') { - return false; - } + if (condition.if.not) { + // return false if any condition column values are defined, when condition is inverted + return false; } - // all conditions met - return true; + // return true if all condition columns are defined + return !columnValueParts.every( + (columnValuePart) => columnValuePart === undefined || columnValuePart === null || columnValuePart === '' + ); } /** @@ -519,24 +555,25 @@ export class XLSXTransformation { const newRowObject = {}; - Object.entries(rowObject).forEach(([dwcField, value]) => { - for (const column of columns) { - if (column.source === dwcField) { - newRowObject[column.target] = value; - break; + for (const column of columns) { + if (column.source.columns && column.source.columns?.length) { + // iterate over source columns + for (const sourceColumn of column.source.columns) { + const sourceValue = rowObject[sourceColumn]; + + if (sourceValue) { + // use the first source column that has a defined value + newRowObject[column.target] = sourceValue; + break; + } } + } else if (column.source.value) { + newRowObject[column.target] = column.source.value; } - }); - - const newRowObjectKeys = Object.keys(newRowObject); + } - if ( - parseSchema?.condition && - !parseSchema?.condition?.if?.columns.every((conditionalFields) => - newRowObjectKeys.includes(conditionalFields) - ) - ) { - // If the `newRowObject` is missing any of the conditional fields, skip this record + if (!Object.keys(newRowObject).length) { + // row object is empty, skip return; } diff --git a/api/src/utils/path-utils.ts b/api/src/utils/path-utils.ts deleted file mode 100644 index 08cd3f66b5..0000000000 --- a/api/src/utils/path-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RequestHandler } from 'express'; -import { getLogger } from './logger'; - -/** - * Logs the contents of the request parms and body, if LOG_LEVEL='debug'. - * - * @export - * @param {string} callingFilePath a string that indicates which file is makign the call. ex: 'paths/endpoint' - * @param {string} httpOperation the HTTP operation being performed. ex: 'POST' - * @return {*} {RequestHandler} - */ -export function logRequest(callingFilePath: string, httpOperation: string): RequestHandler { - const defaultLog = getLogger(callingFilePath); - - return async (req, res, next) => { - defaultLog.debug({ label: httpOperation, message: 'request', 'req.params': req.params, 'req.body': req.body }); - - next(); - }; -} diff --git a/api/src/utils/shared-api-docs.test.ts b/api/src/utils/shared-api-docs.test.ts index b838b41116..2567e070ee 100644 --- a/api/src/utils/shared-api-docs.test.ts +++ b/api/src/utils/shared-api-docs.test.ts @@ -1,9 +1,9 @@ import { expect } from 'chai'; import { describe } from 'mocha'; import { + addFundingSourceApiDocObject, attachmentApiDocObject, - deleteFundingSourceApiDocObject, - addFundingSourceApiDocObject + deleteFundingSourceApiDocObject } from './shared-api-docs'; describe('attachmentApiResponseObject', () => { diff --git a/api/src/utils/shared-api-docs.ts b/api/src/utils/shared-api-docs.ts index 6613223399..ecfa06ad83 100644 --- a/api/src/utils/shared-api-docs.ts +++ b/api/src/utils/shared-api-docs.ts @@ -1,4 +1,3 @@ -import { SYSTEM_ROLE } from '../constants/roles'; import { projectFundingSourcePostRequestObject } from '../openapi/schemas/project-funding-source'; export const attachmentApiDocObject = (basicDescription: string, successDescription: string) => { @@ -7,7 +6,7 @@ export const attachmentApiDocObject = (basicDescription: string, successDescript tags: ['attachment'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -55,7 +54,7 @@ export const deleteFundingSourceApiDocObject = (basicDescription: string, succes tags: ['funding-sources'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -103,7 +102,7 @@ export const addFundingSourceApiDocObject = (basicDescription: string, successDe tags: ['funding-sources'], security: [ { - Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN] + Bearer: [] } ], parameters: [ @@ -130,9 +129,15 @@ export const addFundingSourceApiDocObject = (basicDescription: string, successDe 200: { description: successDescription, content: { - 'text/plain': { + 'application/json': { schema: { - type: 'number' + type: 'object', + required: ['id'], + properties: { + id: { + type: 'number' + } + } } } } diff --git a/api/src/utils/spatial-utils.test.ts b/api/src/utils/spatial-utils.test.ts index d143269dc8..ebe26b659f 100644 --- a/api/src/utils/spatial-utils.test.ts +++ b/api/src/utils/spatial-utils.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { describe } from 'mocha'; -import { parseUTMString } from './spatial-utils'; +import { parseLatLongString, parseUTMString } from './spatial-utils'; describe('parseUTMString', () => { it('returns null when no UTM string provided', async () => { @@ -28,14 +28,26 @@ describe('parseUTMString', () => { expect(result).to.be.null; }); - it('returns null when UTM northing is too small', async () => { - const result = parseUTMString('9N 573674 0'); + it('returns null when UTM northing is too small, for the northern hemisphere', async () => { + const result = parseUTMString('9N 573674 -1'); expect(result).to.be.null; }); - it('returns null when UTM northing is too large', async () => { - const result = parseUTMString('9N 573674 99999999'); + it('returns null when UTM northing is too large, for the northern hemisphere', async () => { + const result = parseUTMString('9N 573674 9334081'); + + expect(result).to.be.null; + }); + + it('returns null when UTM northing is too small, for the southern hemisphere', async () => { + const result = parseUTMString('9C 573674 1110399'); + + expect(result).to.be.null; + }); + + it('returns null when UTM northing is too large, for the southern hemisphere', async () => { + const result = parseUTMString('9C 573674 10000001'); expect(result).to.be.null; }); @@ -82,3 +94,50 @@ describe('parseUTMString', () => { expect(result).to.eql({ easting: 573674, northing: 6114170, zone_letter: 'C', zone_number: 18, zone_srid: 32718 }); }); }); + +describe('parseLatLongString', () => { + it('returns null when no LatLong string provided', async () => { + expect(parseLatLongString((null as unknown) as string)).to.be.null; + expect(parseLatLongString('')).to.be.null; + }); + + it('returns null when provided LatLong string has invalid format', () => { + expect(parseLatLongString('49.1.2 -120')).to.be.null; + expect(parseLatLongString('49.49 -120.1.2')).to.be.null; + expect(parseLatLongString('badLatitude 120')).to.be.null; + expect(parseLatLongString('-49 badLongitude')).to.be.null; + expect(parseLatLongString('49 -120 extra')).to.be.null; + expect(parseLatLongString('')).to.be.null; + expect(parseLatLongString('not a lat long string')).to.be.null; + }); + + it('returns null when latitude is too small', async () => { + const result = parseLatLongString('-91 0'); + + expect(result).to.be.null; + }); + + it('returns null when latitude is too large', async () => { + const result = parseLatLongString('91 0'); + + expect(result).to.be.null; + }); + + it('returns null when longitude is too small', async () => { + const result = parseLatLongString('0 -181'); + + expect(result).to.be.null; + }); + + it('returns null when longitude is too large', async () => { + const result = parseLatLongString('0 181'); + + expect(result).to.be.null; + }); + + it('returns parsed lat long when lat long string is valid', async () => { + const result = parseLatLongString('49.123 -120.123'); + + expect(result).to.eql({ lat: 49.123, long: -120.123 }); + }); +}); diff --git a/api/src/utils/spatial-utils.ts b/api/src/utils/spatial-utils.ts index e039ca6158..ee347d45be 100644 --- a/api/src/utils/spatial-utils.ts +++ b/api/src/utils/spatial-utils.ts @@ -31,26 +31,15 @@ export function parseUTMString(utm: string): IUTM | null { } const utmParts = utm.split(' '); - const easting = Number(utmParts[1]); - if (easting < 166640 || easting > 833360) { - // utm easting is invalid - return null; - } - - const northing = Number(utmParts[2]); - if (northing < 1110400 || northing > 9334080) { - // utm northing is invalid - return null; - } - - const hasZoneLetter = UTM_ZONE_WITH_LETTER_FORMAT.test(utmParts[0]); let zone_letter = undefined; let zone_number = undefined; + const hasZoneLetter = UTM_ZONE_WITH_LETTER_FORMAT.test(utmParts[0]); + if (hasZoneLetter) { - zone_letter = utmParts[0].slice(-1).toUpperCase(); zone_number = Number(utmParts[0].slice(0, -1)); + zone_letter = utmParts[0].slice(-1).toUpperCase(); } else { zone_number = Number(utmParts[0]); } @@ -60,11 +49,30 @@ export function parseUTMString(utm: string): IUTM | null { return null; } + const easting = Number(utmParts[1]); + if (easting < 166640 || easting > 833360) { + // utm easting is invalid + return null; + } + + const northing = Number(utmParts[2]); + let zone_srid; + if (!zone_letter || NORTH_UTM_ZONE_LETTERS.includes(zone_letter)) { + if (northing < 0 || northing > 9334080) { + // utm northing is invalid + return null; + } + // If `zone_letter` is not defined, then assume northern hemisphere zone_srid = NORTH_UTM_BASE_ZONE_NUMBER + zone_number; } else if (SOUTH_UTM_ZONE_LETTERS.includes(zone_letter)) { + if (northing < 1110400 || northing > 10000000) { + // utm northing is invalid + return null; + } + zone_srid = SOPUTH_UTM_BASE_ZONE_NUMBER + zone_number; } else { return null; @@ -72,3 +80,40 @@ export function parseUTMString(utm: string): IUTM | null { return { easting, northing, zone_letter, zone_number, zone_srid }; } + +export interface ILatLong { + lat: number; + long: number; +} + +const LAT_LONG_STRING_FORMAT = RegExp(/^[+-]?(\d*[.])?\d+ [+-]?(\d*[.])?\d+$/i); + +/** + * Parses a `latitude longitude` string of the form: `49.116906 -122.62887` + * + * @export + * @param {string} latLong + * @return {*} {(ILatLong | null)} + */ +export function parseLatLongString(latLong: string): ILatLong | null { + if (!latLong || !LAT_LONG_STRING_FORMAT.test(latLong)) { + // latLong string is null or does not match the expected format + return null; + } + + const latLongParts = latLong.split(' '); + + const lat = Number(latLongParts[0]); + if (lat < -90 || lat > 90) { + // latitude is invalid + return null; + } + + const long = Number(latLongParts[1]); + if (long < -180 || long > 180) { + // longitude is invalid + return null; + } + + return { lat, long }; +} diff --git a/api/tsconfig.json b/api/tsconfig.json index 3e1da06344..1ae8680ddd 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "lib": ["es2018"], + "lib": ["es2020"], "outDir": "dist", - "target": "es5", + "target": "es2018", "sourceMap": true, "allowJs": false, "moduleResolution": "node", diff --git a/app/.docker/app/Dockerfile b/app/.docker/app/Dockerfile index 59f000b53b..d9e896d0cf 100644 --- a/app/.docker/app/Dockerfile +++ b/app/.docker/app/Dockerfile @@ -1,4 +1,4 @@ -FROM node:10 +FROM node:14 ENV HOME=/opt/app-root/src diff --git a/app/.eslintrc b/app/.eslintrc index 1922127f60..2ed75e7d90 100644 --- a/app/.eslintrc +++ b/app/.eslintrc @@ -1,6 +1,29 @@ { - "extends": ["react-app", "plugin:prettier/recommended"], + "extends": [ + "react-app", + "eslint:recommended", + "prettier/@typescript-eslint", + "plugin:prettier/recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["prettier", "@typescript-eslint"], "rules": { - "prettier/prettier": ["warn"] + "prettier/prettier": ["warn"], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/ban-types": ["error", { "types": { "object": false, "extendDefaults": true } }], + "@typescript-eslint/no-empty-function": ["error", { "allow": ["arrowFunctions"] }], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": false, + "ts-ignore": false, + "ts-nocheck": false, + "ts-check": false + } + ], + "no-var": "error" } } diff --git a/app/.gitignore b/app/.gitignore index e18c5eaada..8dbc004cb7 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,7 +1,6 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. node_modules -package-lock.json **/.npm **/.config diff --git a/app/.pipeline/config.js b/app/.pipeline/config.js index 398eaabe41..1f005db4ea 100644 --- a/app/.pipeline/config.js +++ b/app/.pipeline/config.js @@ -25,6 +25,9 @@ const staticUrls = config.staticUrls || {}; const staticUrlsAPI = config.staticUrlsAPI || {}; const staticUrlsN8N = config.staticUrlsN8N || {}; +const maxUploadNumFiles = 10; +const maxUploadFileSize = 52428800; // (bytes) + const sso = config.sso; const processOptions = (options) => { @@ -81,6 +84,8 @@ const phases = { `${apiName}-${changeId}-af2668-dev.apps.silver.devops.gov.bc.ca`, n8nHost: '', // staticUrlsN8N.dev, // Disable until nginx is setup: https://quartech.atlassian.net/browse/BHBC-1435 siteminderLogoutURL: config.siteminderLogoutURL.dev, + maxUploadNumFiles, + maxUploadFileSize, env: 'dev', sso: sso.dev, replicas: 1, @@ -99,6 +104,8 @@ const phases = { apiHost: staticUrlsAPI.test || defaultHostAPI, n8nHost: '', // staticUrlsN8N.test, // Disable until nginx is setup: https://quartech.atlassian.net/browse/BHBC-1435 siteminderLogoutURL: config.siteminderLogoutURL.test, + maxUploadNumFiles, + maxUploadFileSize, env: 'test', sso: sso.test, replicas: 3, @@ -117,6 +124,8 @@ const phases = { apiHost: staticUrlsAPI.prod || defaultHostAPI, n8nHost: '', // staticUrlsN8N.prod, // Disable until nginx is setup: https://quartech.atlassian.net/browse/BHBC-1435 siteminderLogoutURL: config.siteminderLogoutURL.prod, + maxUploadNumFiles, + maxUploadFileSize, env: 'prod', sso: sso.prod, replicas: 3, diff --git a/app/.pipeline/lib/app.deploy.js b/app/.pipeline/lib/app.deploy.js index 0d131ae493..00b9aef0be 100644 --- a/app/.pipeline/lib/app.deploy.js +++ b/app/.pipeline/lib/app.deploy.js @@ -26,6 +26,8 @@ module.exports = (settings) => { REACT_APP_API_HOST: phases[phase].apiHost, REACT_APP_N8N_HOST: phases[phase].n8nHost, REACT_APP_SITEMINDER_LOGOUT_URL: phases[phase].siteminderLogoutURL, + REACT_APP_MAX_UPLOAD_NUM_FILES: phases[phase].maxUploadNumFiles, + REACT_APP_MAX_UPLOAD_FILE_SIZE: phases[phase].maxUploadFileSize, NODE_ENV: phases[phase].env || 'dev', REACT_APP_NODE_ENV: phases[phase].env || 'dev', SSO_URL: phases[phase].sso.url, diff --git a/app/.pipeline/package.json b/app/.pipeline/package.json index 3eb9272098..3541a1ed06 100644 --- a/app/.pipeline/package.json +++ b/app/.pipeline/package.json @@ -4,7 +4,8 @@ "description": "Contains dependencies and scripts for executing OpenShift pipeline build/deploy scripts", "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": ">= 14.0.0", + "npm": ">= 6.0.0" }, "repository": { "type": "git", diff --git a/app/Dockerfile b/app/Dockerfile index 272dd17dbf..92b6266f45 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -2,7 +2,7 @@ # This DockerFile is used for deployments only, please do not change for other purposes. It will break our deployment. # #################################################################################################################### -FROM node:10 +FROM node:14 ENV HOME=/opt/app-root/src @@ -29,4 +29,4 @@ RUN npm run build EXPOSE 7100 # RUN APP -CMD ["npm", "run", "deploy_start"] \ No newline at end of file +CMD ["npm", "run", "deploy_start"] diff --git a/app/README.md b/app/README.md index 286d7b792b..7976b303be 100644 --- a/app/README.md +++ b/app/README.md @@ -1,6 +1,13 @@ # bcgov/biohubbc/app -A standard React web-app for BioHub management activities. +## Technologies Used + +| Technology | Version | Website | Description | +| ---------- | ------- | ------------------------------------ | -------------------- | +| node | 14.x.x | https://nodejs.org/en/ | JavaScript Runtime | +| npm | 6.x.x | https://www.npmjs.com/ | Node Package Manager | + +
## Documenation @@ -123,4 +130,4 @@ The simplest solution for now is to keep typescript at the latest `3.x` version. ``` 0:0 error Parsing error: Cannot read property 'map' of undefined -``` \ No newline at end of file +``` diff --git a/app/openshift/app.bc.yaml b/app/openshift/app.bc.yaml index 13a5883b0e..436a8fa7d0 100644 --- a/app/openshift/app.bc.yaml +++ b/app/openshift/app.bc.yaml @@ -30,13 +30,13 @@ parameters: value: dev - name: BASE_IMAGE_URL required: true - value: registry.access.redhat.com/ubi8/nodejs-10:1-41 + value: image-registry.openshift-image-registry.svc:5000/openshift/nodejs:14-ubi8 - name: SOURCE_IMAGE_NAME required: true - value: redhat-ubi-node-10 + value: nodejs - name: SOURCE_IMAGE_TAG required: true - value: 1-41 + value: 14-ubi8 objects: - kind: ImageStream apiVersion: v1 @@ -88,11 +88,11 @@ objects: postCommit: {} resources: limits: - cpu: 1250m + cpu: 1000m memory: 5Gi requests: - cpu: 750m - memory: 5Gi + cpu: 100m + memory: 512Mi runPolicy: Serial source: contextDir: '${SOURCE_CONTEXT_DIR}' diff --git a/app/openshift/app.dc.yaml b/app/openshift/app.dc.yaml index 162bcab891..a820f9f8cc 100644 --- a/app/openshift/app.dc.yaml +++ b/app/openshift/app.dc.yaml @@ -23,6 +23,12 @@ parameters: - name: REACT_APP_SITEMINDER_LOGOUT_URL description: Siteminder URL to log out and clear the session for the logged in user value: '' + - name: REACT_APP_MAX_UPLOAD_NUM_FILES + description: Default maximum number of files that can be uploaded at a time vai the upload component UI. + value: '10' + - name: REACT_APP_MAX_UPLOAD_FILE_SIZE + description: Default maximum size of a single file that can be uploaded by the upload component UI. + value: '52428800' - name: NODE_ENV description: NODE_ENV specification variable value: 'dev' @@ -35,18 +41,6 @@ parameters: - name: APP_PORT_DEFAULT_NAME description: Default port resource name value: '7100-tcp' - - name: CPU_REQUEST - description: CPU REQUEST for deployment config - value: '200m' - - name: CPU_LIMIT - description: CPU LIMIT for dc - value: '500m' - - name: MEMORY_REQUEST - description: MEMORY REQUEST for dc - value: '1Gi' - - name: MEMORY_LIMIT - description: MEMORY LIMIT for dc - value: '2Gi' - name: SSO_URL description: Key clock login url value: 'https://oidc.gov.bc.ca/auth' @@ -56,6 +50,14 @@ parameters: - name: SSO_REALM description: Realm identifier or name value: 35r1iman + - name: CPU_REQUEST + value: '10m' + - name: CPU_LIMIT + value: '200m' + - name: MEMORY_REQUEST + value: '50Mi' + - name: MEMORY_LIMIT + value: '200Mi' - name: REPLICAS value: '1' - name: REPLICA_MAX @@ -122,6 +124,10 @@ objects: value: ${REACT_APP_N8N_HOST} - name: REACT_APP_SITEMINDER_LOGOUT_URL value: ${REACT_APP_SITEMINDER_LOGOUT_URL} + - name: REACT_APP_MAX_UPLOAD_NUM_FILES + value: ${REACT_APP_MAX_UPLOAD_NUM_FILES} + - name: REACT_APP_MAX_UPLOAD_FILE_SIZE + value: ${REACT_APP_MAX_UPLOAD_FILE_SIZE} - name: NODE_ENV value: ${NODE_ENV} - name: REACT_APP_NODE_ENV diff --git a/app/package-lock.json b/app/package-lock.json index aadfc3117d..dfd27d916f 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,8 +1,22355 @@ { - "name": "biohubbc-app", + "name": "sims-app", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "sims-app", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@bcgov/bc-sans": "~1.0.1", + "@material-ui/core": "~4.11.0", + "@material-ui/icons": "~4.2.1", + "@material-ui/lab": "latest", + "@material-ui/pickers": "~3.2.10", + "@material-ui/styles": "~4.10.0", + "@material-ui/system": "4.11.3", + "@mdi/js": "~6.4.95", + "@mdi/react": "~1.4.0", + "@react-keycloak/web": "~2.1.0", + "@react-leaflet/core": "~1.0.2", + "@tmcw/togeojson": "~4.2.0", + "@turf/bbox": "~6.3.0", + "@turf/boolean-equal": "~6.3.0", + "@turf/centroid": "~6.4.0", + "axios": "~0.21.4", + "clsx": "~1.1.1", + "express": "~4.17.1", + "formik": "~2.2.6", + "keycloak-js": "~9.0.2", + "leaflet": "~1.7.1", + "leaflet-draw": "~1.0.4", + "leaflet-fullscreen": "~1.0.2", + "leaflet.locatecontrol": "~0.76.0", + "lodash-es": "~4.17.21", + "moment": "~2.29.2", + "node-sass": "~4.14.1", + "qs": "~6.9.4", + "react": "~16.14.0", + "react-dom": "~16.14.0", + "react-dropzone": "~11.3.2", + "react-leaflet": "~3.1.0", + "react-leaflet-cluster": "~1.0.3", + "react-number-format": "~4.5.2", + "react-router": "~5.1.2", + "react-router-dom": "~5.1.2", + "react-window": "~1.8.6", + "reproj-helper": "~1.2.8", + "shpjs": "^3.6.3", + "typescript": "~4.1.6", + "uuid": "~8.3.2", + "yup": "~0.32.9" + }, + "devDependencies": { + "@babel/preset-typescript": "~7.12.7", + "@testing-library/jest-dom": "~5.11.8", + "@testing-library/react": "~11.2.3", + "@testing-library/user-event": "~12.6.0", + "@types/geojson": "~7946.0.7", + "@types/jest": "~26.0.20", + "@types/leaflet": "~1.5.23", + "@types/leaflet-fullscreen": "~1.0.6", + "@types/lodash-es": "~4.17.4", + "@types/node": "~14.14.31", + "@types/node-sass": "~4.11.2", + "@types/qs": "~6.9.5", + "@types/react": "~16.9.17", + "@types/react-dom": "~16.9.4", + "@types/react-leaflet": "~2.5.2", + "@types/react-router": "~5.1.4", + "@types/react-router-dom": "~5.1.3", + "@types/react-window": "~1.8.2", + "@types/shpjs": "^3.4.0", + "@types/uuid": "~8.3.0", + "@typescript-eslint/eslint-plugin": "~3.10.1", + "@typescript-eslint/parser": "~3.10.1", + "axios-mock-adapter": "~1.19.0", + "eslint": "~6.8.0", + "eslint-config-prettier": "~6.15.0", + "eslint-plugin-prettier": "~3.3.1", + "jest": "~24.9.0", + "jest-sonar-reporter": "~2.0.0", + "prettier": "~2.2.1", + "prettier-plugin-organize-imports": "~2.3.4", + "react-scripts": "~3.4.4" + }, + "engines": { + "node": ">= 14.0.0", + "npm": ">= 6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", + "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", + "dev": true + }, + "node_modules/@babel/core": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.10.tgz", + "integrity": "sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.10", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz", + "integrity": "sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.10" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", + "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", + "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.12.5", + "@babel/helper-validator-option": "^7.12.1", + "browserslist": "^4.14.5", + "semver": "^5.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", + "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "regexpu-core": "^4.7.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-map": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", + "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.10.4", + "@babel/types": "^7.10.5", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", + "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz", + "integrity": "sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.12.10", + "@babel/template": "^7.12.7", + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz", + "integrity": "sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.10" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", + "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.7" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", + "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.5" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", + "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/helper-validator-identifier": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.1", + "@babel/types": "^7.12.1", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.10" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", + "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-wrap-function": "^7.10.4", + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", + "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz", + "integrity": "sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.1" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.12.11" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", + "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", + "dev": true + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.12.3", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", + "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "node_modules/@babel/helpers": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", + "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.12.5", + "@babel/types": "^7.12.5" + } + }, + "node_modules/@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz", + "integrity": "sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.8.3.tgz", + "integrity": "sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-decorators": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", + "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-dynamic-import": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", + "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", + "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", + "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", + "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", + "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", + "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", + "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", + "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", + "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", + "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", + "integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz", + "integrity": "sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", + "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz", + "integrity": "sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", + "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", + "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-remap-async-to-generator": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", + "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz", + "integrity": "sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", + "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-define-map": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4", + "globals": "^11.1.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", + "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", + "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", + "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", + "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", + "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.9.0.tgz", + "integrity": "sha512-7Qfg0lKQhEHs93FChxVLAvhBshOPQDtJUTVHr/ZwQNRccCm4O9D79r9tVSoV8iNwjP1YgfD+e/fgHcPkN1qEQg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-flow": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", + "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", + "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", + "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", + "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", + "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", + "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-simple-access": "^7.12.1", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", + "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.10.4", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-identifier": "^7.10.4", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", + "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", + "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", + "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", + "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", + "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", + "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz", + "integrity": "sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", + "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz", + "integrity": "sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.12.10", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-jsx": "^7.12.1", + "@babel/types": "^7.12.12" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz", + "integrity": "sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.12.12" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz", + "integrity": "sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz", + "integrity": "sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz", + "integrity": "sha512-RqeaHiwZtphSIUZ5I85PEH19LOSzxfuEazoY7/pWASCAIBuATQzpSVD+eT6MebeeZT2F4eSL0u4vw6n4Nm0Mjg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", + "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "dev": true, + "dependencies": { + "regenerator-transform": "^0.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", + "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.0.tgz", + "integrity": "sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "resolve": "^1.8.1", + "semver": "^5.5.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", + "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", + "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", + "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", + "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz", + "integrity": "sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", + "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-typescript": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", + "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", + "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.12.1", + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.11.tgz", + "integrity": "sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.12.7", + "@babel/helper-compilation-targets": "^7.12.5", + "@babel/helper-module-imports": "^7.12.5", + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.11", + "@babel/plugin-proposal-async-generator-functions": "^7.12.1", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-dynamic-import": "^7.12.1", + "@babel/plugin-proposal-export-namespace-from": "^7.12.1", + "@babel/plugin-proposal-json-strings": "^7.12.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-numeric-separator": "^7.12.7", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-proposal-private-methods": "^7.12.1", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-class-properties": "^7.12.1", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.12.1", + "@babel/plugin-transform-arrow-functions": "^7.12.1", + "@babel/plugin-transform-async-to-generator": "^7.12.1", + "@babel/plugin-transform-block-scoped-functions": "^7.12.1", + "@babel/plugin-transform-block-scoping": "^7.12.11", + "@babel/plugin-transform-classes": "^7.12.1", + "@babel/plugin-transform-computed-properties": "^7.12.1", + "@babel/plugin-transform-destructuring": "^7.12.1", + "@babel/plugin-transform-dotall-regex": "^7.12.1", + "@babel/plugin-transform-duplicate-keys": "^7.12.1", + "@babel/plugin-transform-exponentiation-operator": "^7.12.1", + "@babel/plugin-transform-for-of": "^7.12.1", + "@babel/plugin-transform-function-name": "^7.12.1", + "@babel/plugin-transform-literals": "^7.12.1", + "@babel/plugin-transform-member-expression-literals": "^7.12.1", + "@babel/plugin-transform-modules-amd": "^7.12.1", + "@babel/plugin-transform-modules-commonjs": "^7.12.1", + "@babel/plugin-transform-modules-systemjs": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", + "@babel/plugin-transform-new-target": "^7.12.1", + "@babel/plugin-transform-object-super": "^7.12.1", + "@babel/plugin-transform-parameters": "^7.12.1", + "@babel/plugin-transform-property-literals": "^7.12.1", + "@babel/plugin-transform-regenerator": "^7.12.1", + "@babel/plugin-transform-reserved-words": "^7.12.1", + "@babel/plugin-transform-shorthand-properties": "^7.12.1", + "@babel/plugin-transform-spread": "^7.12.1", + "@babel/plugin-transform-sticky-regex": "^7.12.7", + "@babel/plugin-transform-template-literals": "^7.12.1", + "@babel/plugin-transform-typeof-symbol": "^7.12.10", + "@babel/plugin-transform-unicode-escapes": "^7.12.1", + "@babel/plugin-transform-unicode-regex": "^7.12.1", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.12.11", + "core-js-compat": "^3.8.0", + "semver": "^5.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", + "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.10.tgz", + "integrity": "sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-transform-react-display-name": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.10", + "@babel/plugin-transform-react-jsx-development": "^7.12.7", + "@babel/plugin-transform-react-pure-annotations": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz", + "integrity": "sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-validator-option": "^7.12.1", + "@babel/plugin-transform-typescript": "^7.12.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", + "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", + "dev": true, + "dependencies": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "node_modules/@babel/template": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", + "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.12.7", + "@babel/types": "^7.12.7" + } + }, + "node_modules/@babel/traverse": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz", + "integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@babel/generator": "^7.12.11", + "@babel/helper-function-name": "^7.12.11", + "@babel/helper-split-export-declaration": "^7.12.11", + "@babel/parser": "^7.12.11", + "@babel/types": "^7.12.12", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.12.12", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", + "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + }, + "node_modules/@bcgov/bc-sans": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bcgov/bc-sans/-/bc-sans-1.0.1.tgz", + "integrity": "sha512-4suRUBFeHcuFkwXXJu9pKJNB5Z2G3bpuLEHIq203KVCKC8KrsnqvsyUOf645TypgLwqOTOYCETiXYzfxF4gLAw==" + }, + "node_modules/@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "dependencies": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "bin": { + "watch": "cli.js" + }, + "engines": { + "node": ">=0.1.95" + } + }, + "node_modules/@csstools/convert-colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", + "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@csstools/normalize.css": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", + "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==", + "dev": true + }, + "node_modules/@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "deprecated": "Moved to 'npm install @sideway/address'", + "dev": true + }, + "node_modules/@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true + }, + "node_modules/@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true + }, + "node_modules/@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "deprecated": "Switch to 'npm install joi'", + "dev": true, + "dependencies": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "node_modules/@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true, + "dependencies": { + "@hapi/hoek": "^8.3.0" + } + }, + "node_modules/@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "dependencies": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/core/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/environment/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/environment/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/fake-timers/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/fake-timers/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/reporters/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/reporters/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/reporters/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/reporters/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/source-map/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/test-result/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/test-result/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/test-result/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "dependencies": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@jest/transform/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/transform/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@material-ui/core": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.11.0.tgz", + "integrity": "sha512-bYo9uIub8wGhZySHqLQ833zi4ZML+XCBE1XwJ8EuUVSpTWWG57Pm+YugQToJNFsEyiKFhPh8DPD0bgupz8n01g==", + "deprecated": "You can now upgrade to @mui/material. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.10.0", + "@material-ui/system": "^4.9.14", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.10.2", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6", + "react": "^16.8.0", + "react-dom": "^16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/core/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/@material-ui/icons": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.2.1.tgz", + "integrity": "sha512-FvSD5lUBJ66frI4l4AYAPy2CH14Zs2Dgm0o3oOMr33BdQtOAjCgbdOcvPBeaD1w6OQl31uNW3CKOE8xfPNxvUQ==", + "deprecated": "You can now upgrade to @mui/icons. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.2.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.1.2", + "react": "^16.8.0", + "react-dom": "^16.8.0" + } + }, + "node_modules/@material-ui/lab": { + "version": "4.0.0-alpha.60", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz", + "integrity": "sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==", + "deprecated": "You can now upgrade to @mui/lab. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.12.1", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/pickers": { + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz", + "integrity": "sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w==", + "dependencies": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + }, + "peerDependencies": { + "@date-io/core": "^1.3.6", + "@material-ui/core": "^4.0.0", + "prop-types": "^15.6.0", + "react": "^16.8.4", + "react-dom": "^16.8.4" + } + }, + "node_modules/@material-ui/styles": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.10.0.tgz", + "integrity": "sha512-XPwiVTpd3rlnbfrgtEJ1eJJdFCXZkHxy8TrdieaTvwxNYj42VnnCyFzxYeNW9Lhj4V1oD8YtQ6S5Gie7bZDf7Q==", + "deprecated": "You can now upgrade to @mui/styles. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "^5.1.0", + "@material-ui/utils": "^4.9.6", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.0.3", + "jss-plugin-camel-case": "^10.0.3", + "jss-plugin-default-unit": "^10.0.3", + "jss-plugin-global": "^10.0.3", + "jss-plugin-nested": "^10.0.3", + "jss-plugin-props-sort": "^10.0.3", + "jss-plugin-rule-value-function": "^10.0.3", + "jss-plugin-vendor-prefixer": "^10.0.3", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6", + "react": "^16.8.0", + "react-dom": "^16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/system": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.11.3.tgz", + "integrity": "sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw==", + "deprecated": "You can now upgrade to @mui/system. See the guide: https://mui.com/guides/migration-v4/", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.2", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/utils": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.2.tgz", + "integrity": "sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@mdi/js": { + "version": "6.4.95", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.4.95.tgz", + "integrity": "sha512-b1/P//1D2KOzta8YRGyoSLGsAlWyUHfxzVBhV4e/ppnjM4DfBgay/vWz7Eg5Ee80JZ4zsQz8h54X+KOahtBk5Q==" + }, + "node_modules/@mdi/react": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.4.0.tgz", + "integrity": "sha512-OUH9RhfDJPhybQL3owwrSDIXz2yVKXg5lYeOZjyRCiT9wqywNK0FeYyDByOwNIZnnIQoQYmuSrMv+pOX0Uqkmw==" + }, + "node_modules/@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@react-keycloak/core": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@react-keycloak/core/-/core-2.2.1.tgz", + "integrity": "sha512-MKoawiLMamhCrcEQ79svZZV8lw+ojkcz4LMGeCzHULlZB8sTDY3uBg7S8NxhpdPdyAjQoQ5ExKL4Il75OBIamg==", + "dependencies": { + "@babel/runtime": "^7.9.0", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.0.1" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/reactkeycloak" + }, + "peerDependencies": { + "keycloak-js": ">=9.0.2", + "react": ">=16" + } + }, + "node_modules/@react-keycloak/web": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@react-keycloak/web/-/web-2.1.4.tgz", + "integrity": "sha512-HZry6/UIeStGc2reqYhRFAkmVGCb4fQfOdOK2bJ6RCatN5uuJ+sDu5w5L97JtGNirTqP2eKsTUM3Dx1EvgJ4ng==", + "dependencies": { + "@babel/runtime": "^7.9.0", + "@react-keycloak/core": "^2.2.1", + "hoist-non-react-statics": "^3.3.2", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/reactkeycloak" + }, + "peerDependencies": { + "keycloak-js": ">=9.0.2", + "react": ">=16" + } + }, + "node_modules/@react-leaflet/core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-1.0.2.tgz", + "integrity": "sha512-QbleYZTMcgujAEyWGki8Lx6cXQqWkNtQlqf5c7NImlIp8bKW66bFpez/6EVatW7+p9WKBOEOVci/9W7WW70EZg==", + "peerDependencies": { + "leaflet": "^1.7.1", + "react": "^17.0.1", + "react-dom": "^17.0.1" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz", + "integrity": "sha512-j7KnilGyZzYr/jhcrSYS3FGWMZVaqyCG0vzMCwzvei0coIkczuYMcniK07nI0aHJINciujjH11T72ICW5eL5Ig==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-4.2.0.tgz", + "integrity": "sha512-3XHLtJ+HbRCH4n28S7y/yZoEQnRpl0tvTZQsHqvaeNXPra+6vE5tbRliH3ox1yZYPCxrlqaJT/Mg+75GpDKlvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-4.2.0.tgz", + "integrity": "sha512-yTr2iLdf6oEuUE9MsRdvt0NmdpMBAkgK8Bjhl6epb+eQWk6abBaX3d65UZ3E3FWaOwePyUgNyNCMVG61gGCQ7w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-4.2.0.tgz", + "integrity": "sha512-U9m870Kqm0ko8beHawRXLGLvSi/ZMrl89gJ5BNcT452fAjtF2p4uRzXkdzvGJJJYBgx7BmqlDjBN/eCp5AAX2w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-4.3.3.tgz", + "integrity": "sha512-w3Be6xUNdwgParsvxkkeZb545VhXEwjGMwExMVBIdPQJeyMQHqm9Msnb2a1teHBqUYL66qtwfhNkbj1iarCG7w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-4.2.0.tgz", + "integrity": "sha512-C0Uy+BHolCHGOZ8Dnr1zXy/KgpBOkEUYY9kI/HseHVPeMbluaX3CijJr7D4C5uR8zrc1T64nnq/k63ydQuGt4w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-4.2.0.tgz", + "integrity": "sha512-7YvynOpZDpCOUoIVlaaOUU87J4Z6RdD6spYN4eUb5tfPoKGSF9OG2NuhgYnq4jSkAxcpMaXWPf1cePkzmqTPNw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-4.2.0.tgz", + "integrity": "sha512-hYfYuZhQPCBVotABsXKSCfel2slf/yvJY8heTVX1PCTaq/IgASq1IyxPPKJ0chWREEKewIU/JMSsIGBtK1KKxw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-4.3.3.tgz", + "integrity": "sha512-6PG80tdz4eAlYUN3g5GZiUjg2FMcp+Wn6rtnz5WJG9ITGEF1pmFdzq02597Hn0OmnQuCVaBYQE1OVFAnwOl+0A==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^4.2.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^4.2.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^4.2.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^4.2.0", + "@svgr/babel-plugin-svg-dynamic-title": "^4.3.3", + "@svgr/babel-plugin-svg-em-dimensions": "^4.2.0", + "@svgr/babel-plugin-transform-react-native-svg": "^4.2.0", + "@svgr/babel-plugin-transform-svg-component": "^4.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/core": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-4.3.3.tgz", + "integrity": "sha512-qNuGF1QON1626UCaZamWt5yedpgOytvLj5BQZe2j1k1B8DUG4OyugZyfEwBeXozCUwhLEpsrgPrE+eCu4fY17w==", + "dev": true, + "dependencies": { + "@svgr/plugin-jsx": "^4.3.3", + "camelcase": "^5.3.1", + "cosmiconfig": "^5.2.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-4.3.2.tgz", + "integrity": "sha512-JioXclZGhFIDL3ddn4Kiq8qEqYM2PyDKV0aYno8+IXTLuYt6TOgHUbUAAFvqtb0Xn37NwP0BTHglejFoYr8RZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.4.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-4.3.3.tgz", + "integrity": "sha512-cLOCSpNWQnDB1/v+SUENHH7a0XY09bfuMKdq9+gYvtuwzC2rU4I0wKGFEp1i24holdQdwodCtDQdFtJiTCWc+w==", + "dev": true, + "dependencies": { + "@babel/core": "^7.4.5", + "@svgr/babel-preset": "^4.3.3", + "@svgr/hast-util-to-babel-ast": "^4.3.2", + "svg-parser": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-4.3.1.tgz", + "integrity": "sha512-PrMtEDUWjX3Ea65JsVCwTIXuSqa3CG9px+DluF1/eo9mlDrgrtFE7NE/DjdhjJgSM9wenlVBzkzneSIUgfUI/w==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.2.1", + "merge-deep": "^3.0.2", + "svgo": "^1.2.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@svgr/webpack": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-4.3.3.tgz", + "integrity": "sha512-bjnWolZ6KVsHhgyCoYRFmbd26p8XVbulCzSG53BDQqAr+JOAderYK7CuYrB3bDjHJuF6LJ7Wrr42+goLRV9qIg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.4.5", + "@babel/plugin-transform-react-constant-elements": "^7.0.0", + "@babel/preset-env": "^7.4.5", + "@babel/preset-react": "^7.0.0", + "@svgr/core": "^4.3.3", + "@svgr/plugin-jsx": "^4.3.3", + "@svgr/plugin-svgo": "^4.3.1", + "loader-utils": "^1.2.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom": { + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.29.4.tgz", + "integrity": "sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^4.2.0", + "aria-query": "^4.2.2", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.4", + "lz-string": "^1.4.4", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "5.11.9", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.11.9.tgz", + "integrity": "sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^4.2.2", + "chalk": "^3.0.0", + "css": "^3.0.0", + "css.escape": "^1.5.1", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.3.tgz", + "integrity": "sha512-BirBUGPkTW28ULuCwIbYo0y2+0aavHczBT6N9r3LrsswEW3pg25l1wgoE7I8QBIy1upXWkwKpYdWY7NYYP0Bxw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^7.28.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@testing-library/user-event": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.6.0.tgz", + "integrity": "sha512-FNEH/HLmOk5GO70I52tKjs7WvGYckeE/SrnLX/ip7z2IGbffyd5zOUM1tZ10vsTphqm+VbDFI0oaXu0wcfQsAQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tmcw/togeojson": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tmcw/togeojson/-/togeojson-4.2.0.tgz", + "integrity": "sha512-kjUJemmidx4SS9QT4P5nUkS6s9IE0xpqwaWcnDd7q0McFhfRiAHJMEVarxcFXecvZAIyPt9+974NfOGr7/cKfg==" + }, + "node_modules/@turf/bbox": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.3.0.tgz", + "integrity": "sha512-N4ue5Xopu1qieSHP2MA/CJGWHPKaTrVXQJjzHRNcY1vtsO126xbSaJhWUrFc5x5vVkXp0dcucGryO0r5m4o/KA==", + "dependencies": { + "@turf/helpers": "^6.3.0", + "@turf/meta": "^6.3.0" + } + }, + "node_modules/@turf/boolean-equal": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-6.3.0.tgz", + "integrity": "sha512-eXr3oSHTvJYGyu/v57uNg0tnDHFnu+triwAaXtBh7lozt4d2riU8Ow71B+tjT9mBe/JRFfXIDsBWjbyB37y/6w==", + "dependencies": { + "@turf/clean-coords": "^6.3.0", + "@turf/helpers": "^6.3.0", + "@turf/invariant": "^6.3.0", + "geojson-equality": "0.1.6" + } + }, + "node_modules/@turf/centroid": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-6.4.0.tgz", + "integrity": "sha512-p78MVeJ3InVZzkBP4rpoWTUspsRqsW6a/fGuigfjizHz+YqTRXyG7HDkqoR8IwLwpQC83Nlw5kyacgMlgEbN+Q==", + "dependencies": { + "@turf/helpers": "^6.4.0", + "@turf/meta": "^6.4.0" + } + }, + "node_modules/@turf/centroid/node_modules/@turf/helpers": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.4.0.tgz", + "integrity": "sha512-7vVpWZwHP0Qn8DDSlM++nhs3/6zfPt+GODjvLVZ+sWIG4S3vOtUUOfO5eIjRzxsUHHqhgiIL0QA17u79uLM+mQ==" + }, + "node_modules/@turf/centroid/node_modules/@turf/meta": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.4.0.tgz", + "integrity": "sha512-fMra6vMskwz1knn0/tb22ppOeE8CCmpvOvTIxLdV1WYWAoC4bJ4WdXKvZRsJKpHOX5iFehx4DT8aaGdROA4Y3Q==", + "dependencies": { + "@turf/helpers": "^6.4.0" + } + }, + "node_modules/@turf/clean-coords": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-6.3.0.tgz", + "integrity": "sha512-Ns7+vXHigKTclzqlFrUnXsXjtEWAu2YYurDxD5mrKXcncuisUIoKbFM55ZxeiiBj0ji8c1huR1xSqs8GVxZJJA==", + "dependencies": { + "@turf/helpers": "^6.3.0", + "@turf/invariant": "^6.3.0" + } + }, + "node_modules/@turf/helpers": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.3.0.tgz", + "integrity": "sha512-kr6KuD4Z0GZ30tblTEvi90rvvVNlKieXuMC8CTzE/rVQb0/f/Cb29zCXxTD7giQTEQY/P2nRW23wEqqyNHulCg==" + }, + "node_modules/@turf/invariant": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.3.0.tgz", + "integrity": "sha512-2OFOi9p+QOrcIMySEnr+WlOiKaFZ1bY56jA98YyECewJHfhPFWUBZEhc4nWGRT0ahK08Vus9+gcuBX8QIpCIIw==", + "dependencies": { + "@turf/helpers": "^6.3.0" + } + }, + "node_modules/@turf/meta": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.3.0.tgz", + "integrity": "sha512-qBJjaAJS9H3ap0HlGXyF/Bzfl0qkA9suafX/jnDsZvWMfVLt+s+o6twKrXOGk5t7nnNON2NFRC8+czxpu104EQ==", + "dependencies": { + "@turf/helpers": "^6.3.0" + } + }, + "node_modules/@types/aria-query": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz", + "integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.12.tgz", + "integrity": "sha512-wMTHiiTiBAAPebqaPiPDLFA4LYPKr6Ph0Xq/6rq1Ur3v66HXyG+clfR9CNETkD7MQS8ZHvpQOtA53DLws5WAEQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", + "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", + "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.0.tgz", + "integrity": "sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "node_modules/@types/geojson": { + "version": "7946.0.7", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", + "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", + "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "26.0.20", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz", + "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==", + "dev": true, + "dependencies": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, + "node_modules/@types/leaflet": { + "version": "1.5.23", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.23.tgz", + "integrity": "sha512-S/xpuwjZuwYMP+4ZzQ10PX0Jy+0XmwPeojtjqhbca9UXaINdoru91Qm/DUUXyh4qYm3CP6Vher06l/UcA9tUKQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet-fullscreen": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/leaflet-fullscreen/-/leaflet-fullscreen-1.0.6.tgz", + "integrity": "sha512-Kd0T+YDJgtiY02iwjbt2zntGdzGZ+/4MspAJchu5WXz1uoE4EE1K4zmVAOjKFTX/fwzA6OTZBmefVyE3H+HSYg==", + "dev": true, + "dependencies": { + "@types/leaflet": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.4.tgz", + "integrity": "sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "14.14.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.45.tgz", + "integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==", + "dev": true + }, + "node_modules/@types/node-sass": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.2.tgz", + "integrity": "sha512-pOFlTw/OtZda4e+yMjq6/QYuvY0RDMQ+mxXdWj7rfSyf18V8hS4SfgurO+MasAkQsv6Wt6edOGlwh5QqJml9gw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" + }, + "node_modules/@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "16.9.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.56.tgz", + "integrity": "sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "16.9.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.10.tgz", + "integrity": "sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==", + "dev": true, + "dependencies": { + "@types/react": "^16" + } + }, + "node_modules/@types/react-leaflet": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@types/react-leaflet/-/react-leaflet-2.5.2.tgz", + "integrity": "sha512-XBNFsBm4wQiz6BpzUMJAMXru+h2ESyW/mAdPoSzEirtF/g0NOwbUvPYnZtpbiW70AEXT40ZONtsu3lxwr/yliA==", + "dev": true, + "dependencies": { + "@types/leaflet": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz", + "integrity": "sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.6.tgz", + "integrity": "sha512-gjrxYqxz37zWEdMVvQtWPFMFj1dRDb4TGOcgyOfSXTrEXdF92L00WE3C471O3TV/RF1oskcStkXsOU0Ete4s/g==", + "dev": true, + "dependencies": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.2.tgz", + "integrity": "sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + }, + "node_modules/@types/shpjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/shpjs/-/shpjs-3.4.0.tgz", + "integrity": "sha512-ObE0ALHSE+Wj86AuOnscJsOVRxDKOEH0gU6AutNa4tQUlnKhjzdgO9MFl+VCfMg3Xq4FTAteY7tXPmSwGSHwMQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "node_modules/@types/styled-jsx": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz", + "integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.9.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz", + "integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, + "node_modules/@types/uuid": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", + "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^3.0.0", + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "dev": true, + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", + "debug": "^4.1.1", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", + "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", + "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", + "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", + "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", + "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", + "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", + "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "mamacro": "^0.0.3" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", + "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", + "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", + "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", + "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", + "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", + "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/helper-wasm-section": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-opt": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "@webassemblyjs/wast-printer": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", + "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", + "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-buffer": "1.8.5", + "@webassemblyjs/wasm-gen": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", + "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-wasm-bytecode": "1.8.5", + "@webassemblyjs/ieee754": "1.8.5", + "@webassemblyjs/leb128": "1.8.5", + "@webassemblyjs/utf8": "1.8.5" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", + "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/floating-point-hex-parser": "1.8.5", + "@webassemblyjs/helper-api-error": "1.8.5", + "@webassemblyjs/helper-code-frame": "1.8.5", + "@webassemblyjs/helper-fsm": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", + "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/wast-parser": "1.8.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "dependencies": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", + "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-3.0.0.tgz", + "integrity": "sha512-YBrGyT2/uVQ/c6Rr+t6ZJXniY03YtHGMJQYal368burRGYKqhx9qGTWqcBU5s1CwYY9E/ri63RYyG1IacMZtqw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ajv/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", + "dev": true + }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "engines": { + "node": ">=0.4.2" + } + }, + "node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-html": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", + "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "node_modules/are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/arity-n": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arity-n/-/arity-n-1.0.4.tgz", + "integrity": "sha1-2edrEXM+CFacCEeuezmyhgswt0U=", + "dev": true + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/array-includes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.2.tgz", + "integrity": "sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "get-intrinsic": "^1.0.1", + "is-string": "^1.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", + "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true + }, + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "engines": { + "node": "*" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", + "dev": true, + "dependencies": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/axios-mock-adapter": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz", + "integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.3" + }, + "peerDependencies": { + "axios": ">= 0.9.0" + } + }, + "node_modules/axios-mock-adapter/node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "eslint": ">= 4.12.1" + } + }, + "node_modules/babel-extract-comments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-extract-comments/-/babel-extract-comments-1.0.0.tgz", + "integrity": "sha512-qWWzi4TlddohA91bFwgt6zO/J0X+io7Qp184Fw0m2JYRSTZnJbFR8+07KmzudHCZgOiKRCrjhylwv9Xd8gfhVQ==", + "dev": true, + "dependencies": { + "babylon": "^6.18.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "dependencies": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-jest/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/babel-jest/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/babel-loader": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", + "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^2.1.0", + "loader-utils": "^1.4.0", + "mkdirp": "^0.5.3", + "pify": "^4.0.1", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 6.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "dependencies": { + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-macros/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-macros/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-macros/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-plugin-named-asset-import": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", + "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.1.0" + } + }, + "node_modules/babel-plugin-syntax-object-rest-spread": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", + "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", + "dev": true + }, + "node_modules/babel-plugin-transform-object-rest-spread": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz", + "integrity": "sha1-DzZpLVD+9rfi1LOsFHgTepY7ewY=", + "dev": true, + "dependencies": { + "babel-plugin-syntax-object-rest-spread": "^6.8.0", + "babel-runtime": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-react-remove-prop-types": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", + "integrity": "sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==", + "dev": true + }, + "node_modules/babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-react-app": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-9.1.2.tgz", + "integrity": "sha512-k58RtQOKH21NyKtzptoAvtAODuAJJs3ZhqBMl456/GnXEQ/0La92pNmwgWoMn5pBTrsvk3YYXdY7zpY4e3UIxA==", + "dev": true, + "dependencies": { + "@babel/core": "7.9.0", + "@babel/plugin-proposal-class-properties": "7.8.3", + "@babel/plugin-proposal-decorators": "7.8.3", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3", + "@babel/plugin-proposal-numeric-separator": "7.8.3", + "@babel/plugin-proposal-optional-chaining": "7.9.0", + "@babel/plugin-transform-flow-strip-types": "7.9.0", + "@babel/plugin-transform-react-display-name": "7.8.3", + "@babel/plugin-transform-runtime": "7.9.0", + "@babel/preset-env": "7.9.0", + "@babel/preset-react": "7.9.1", + "@babel/preset-typescript": "7.9.0", + "@babel/runtime": "7.9.0", + "babel-plugin-macros": "2.8.0", + "babel-plugin-transform-react-remove-prop-types": "0.4.24" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz", + "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", + "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz", + "integrity": "sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/preset-env": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.9.0.tgz", + "integrity": "sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.9.0", + "@babel/helper-compilation-targets": "^7.8.7", + "@babel/helper-module-imports": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-proposal-async-generator-functions": "^7.8.3", + "@babel/plugin-proposal-dynamic-import": "^7.8.3", + "@babel/plugin-proposal-json-strings": "^7.8.3", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-proposal-numeric-separator": "^7.8.3", + "@babel/plugin-proposal-object-rest-spread": "^7.9.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.8.3", + "@babel/plugin-proposal-optional-chaining": "^7.9.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.8.3", + "@babel/plugin-syntax-async-generators": "^7.8.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-numeric-separator": "^7.8.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.0", + "@babel/plugin-syntax-top-level-await": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.8.3", + "@babel/plugin-transform-async-to-generator": "^7.8.3", + "@babel/plugin-transform-block-scoped-functions": "^7.8.3", + "@babel/plugin-transform-block-scoping": "^7.8.3", + "@babel/plugin-transform-classes": "^7.9.0", + "@babel/plugin-transform-computed-properties": "^7.8.3", + "@babel/plugin-transform-destructuring": "^7.8.3", + "@babel/plugin-transform-dotall-regex": "^7.8.3", + "@babel/plugin-transform-duplicate-keys": "^7.8.3", + "@babel/plugin-transform-exponentiation-operator": "^7.8.3", + "@babel/plugin-transform-for-of": "^7.9.0", + "@babel/plugin-transform-function-name": "^7.8.3", + "@babel/plugin-transform-literals": "^7.8.3", + "@babel/plugin-transform-member-expression-literals": "^7.8.3", + "@babel/plugin-transform-modules-amd": "^7.9.0", + "@babel/plugin-transform-modules-commonjs": "^7.9.0", + "@babel/plugin-transform-modules-systemjs": "^7.9.0", + "@babel/plugin-transform-modules-umd": "^7.9.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3", + "@babel/plugin-transform-new-target": "^7.8.3", + "@babel/plugin-transform-object-super": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.8.7", + "@babel/plugin-transform-property-literals": "^7.8.3", + "@babel/plugin-transform-regenerator": "^7.8.7", + "@babel/plugin-transform-reserved-words": "^7.8.3", + "@babel/plugin-transform-shorthand-properties": "^7.8.3", + "@babel/plugin-transform-spread": "^7.8.3", + "@babel/plugin-transform-sticky-regex": "^7.8.3", + "@babel/plugin-transform-template-literals": "^7.8.3", + "@babel/plugin-transform-typeof-symbol": "^7.8.4", + "@babel/plugin-transform-unicode-regex": "^7.8.3", + "@babel/preset-modules": "^0.1.3", + "@babel/types": "^7.9.0", + "browserslist": "^4.9.1", + "core-js-compat": "^3.6.2", + "invariant": "^2.2.2", + "levenary": "^1.1.1", + "semver": "^5.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/preset-react": { + "version": "7.9.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.9.1.tgz", + "integrity": "sha512-aJBYF23MPj0RNdp/4bHnAP0NVqqZRr9kl0NAOP4nJCex6OYVio59+dnQzsAWFuogdLyeaKA1hmfUIVZkY5J+TQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-transform-react-display-name": "^7.8.3", + "@babel/plugin-transform-react-jsx": "^7.9.1", + "@babel/plugin-transform-react-jsx-development": "^7.9.0", + "@babel/plugin-transform-react-jsx-self": "^7.9.0", + "@babel/plugin-transform-react-jsx-source": "^7.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/preset-typescript": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.9.0.tgz", + "integrity": "sha512-S4cueFnGrIbvYJgwsVFKdvOmpiL0XGw9MFW9D0vgRys5g36PBhZRL8NX8Gr2akz8XRtzq6HuDXPD/1nniagNUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-transform-typescript": "^7.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-preset-react-app/node_modules/@babel/runtime": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.0.tgz", + "integrity": "sha512-cTIudHnzuWLS56ik4DnRnqqNf8MkdUzV4iFFI1h7Jo9xvrpQROYaAnaSd2mHLQAzzZAPfATynX5ord6YlNYNMA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.4" + } + }, + "node_modules/babel-preset-react-app/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/babel-preset-react-app/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dependencies": { + "inherits": "~2.0.0" + }, + "engines": { + "node": "0.4 || >=0.5.8" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bn.js": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/bonjour/node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "node_modules/browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "dependencies": { + "resolve": "1.1.7" + } + }, + "node_modules/browser-resolve/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "dev": true, + "dependencies": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "dev": true, + "dependencies": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.1.tgz", + "integrity": "sha512-UXhDrwqsNcpTYJBTZsbGATDxZbiVDsx6UjpmRUmtnP10pr8wAYr5LgFoEFw9ixriQH2mv/NX2SfGzE/o8GndLA==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001173", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.634", + "escalade": "^3.1.1", + "node-releases": "^1.1.69" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", + "integrity": "sha512-5ZvAxd05HDDU+y9BVvcqYu2LLXmPnQ0hW62h32g4xBTgL/MppR4/04NHfj/ycM2y6lmTnbw6HVi+1eN0Psba6w==", + "dev": true, + "dependencies": { + "chownr": "^1.1.2", + "figgy-pudding": "^3.5.1", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "infer-owner": "^1.0.4", + "lru-cache": "^5.1.1", + "minipass": "^3.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "p-map": "^3.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^2.7.1", + "ssri": "^7.0.0", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dependencies": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001323", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz", + "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "dependencies": { + "rsvp": "^4.8.4" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.3.0.tgz", + "integrity": "sha512-/4YgnZS8y1UXXmC02xD5rRrBEu6T5ub+mQHLNRj0fzTRbgdBYhsNo2V5EqwgqrExjxsjtF/OpAKAMkKsxbD5XQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/chokidar/node_modules/anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/chokidar/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/chokidar/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/chokidar/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chokidar/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/chrome-trace-event/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", + "dev": true, + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", + "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.1", + "color-string": "^1.5.4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz", + "integrity": "sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/compose-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compose-function/-/compose-function-3.0.3.tgz", + "integrity": "sha1-ntZ18TzFRQHTCVCkhv9qe6OrGF8=", + "dev": true, + "dependencies": { + "arity-n": "^1.0.4" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.10.tgz", + "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "node_modules/contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/core-js-compat": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.3.tgz", + "integrity": "sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog==", + "dev": true, + "dependencies": { + "browserslist": "^4.16.1", + "semver": "7.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/core-js-pure": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.8.3.tgz", + "integrity": "sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dependencies": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "node_modules/cross-spawn/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/cross-spawn/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-blank-pseudo": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", + "integrity": "sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "bin": { + "css-blank-pseudo": "cli.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + }, + "engines": { + "node": ">4" + } + }, + "node_modules/css-has-pseudo": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz", + "integrity": "sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^5.0.0-rc.4" + }, + "bin": { + "css-has-pseudo": "cli.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "dependencies": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz", + "integrity": "sha512-jYq4zdZT0oS0Iykt+fqnzVLRIeiPWhka+7BqPn+oSIpWJAHak5tmB/WZrJ2a21JhCeFyNnnlroSl8c+MtVndzA==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.23", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.1", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.2", + "schema-utils": "^2.6.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/css-loader/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", + "integrity": "sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "bin": { + "css-prefers-color-scheme": "cli.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", + "dev": true + }, + "node_modules/css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssdb": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-4.4.0.tgz", + "integrity": "sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz", + "integrity": "sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.7", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-preset-default": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz", + "integrity": "sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.2", + "postcss-unique-selectors": "^4.0.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz", + "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/csso/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "dependencies": { + "cssom": "0.3.x" + } + }, + "node_modules/csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==" + }, + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cyclist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", + "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", + "dev": true + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true + }, + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "dependencies": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" + }, + "engines": { + "node": ">= 4.2.1" + } + }, + "node_modules/diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dir-glob/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "node_modules/dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz", + "integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==", + "dev": true + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-helpers/node_modules/csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/electron-to-chromium": { + "version": "1.3.642", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz", + "integrity": "sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ==", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "dependencies": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.1", + "object-inspect": "^1.9.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "dev": true, + "dependencies": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dev": true, + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-config-prettier/node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-app": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-5.2.1.tgz", + "integrity": "sha512-pGIZ8t0mFLcV+6ZirRgYK6RVqUIKRIi9MmgzUEmrIknsn3AdO0I32asO86dJgloHq+9ZPl8UIg8mYrvgP5u2wQ==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.9" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "2.x", + "@typescript-eslint/parser": "2.x", + "babel-eslint": "10.x", + "eslint": "6.x", + "eslint-plugin-flowtype": "3.x || 4.x", + "eslint-plugin-import": "2.x", + "eslint-plugin-jsx-a11y": "6.x", + "eslint-plugin-react": "7.x", + "eslint-plugin-react-hooks": "1.x || 2.x" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", + "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + } + }, + "node_modules/eslint-loader": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-3.0.3.tgz", + "integrity": "sha512-+YRqB95PnNvxNp1HEjQmvf9KNvCin5HXYYseOXVC2U0KEcw4IkQ2IQEBG46j7+gW39bMzeu0GsUhVbBY3Votpw==", + "deprecated": "This loader has been deprecated. Please use eslint-webpack-plugin", + "dev": true, + "dependencies": { + "fs-extra": "^8.1.0", + "loader-fs-cache": "^1.0.2", + "loader-utils": "^1.2.3", + "object-hash": "^2.0.1", + "schema-utils": "^2.6.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0", + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", + "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "dependencies": { + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-flowtype": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-4.6.0.tgz", + "integrity": "sha512-W5hLjpFfZyZsXfo5anlu7HM970JBDqbEshAJUkeczP6BFCIfJXuiIBQXyberLRtOStT0OGPF8efeTbxlHk4LpQ==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": ">=6.1.0" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "dev": true, + "dependencies": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "2.x - 6.x" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "dependencies": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-import/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.2.3.tgz", + "integrity": "sha512-CawzfGt9w83tyuVekn0GDPU9ytYtxyxyFZ3aSWROmnRRFQFT2BiPJd7jvRdzNDi6oLWaS2asMeYSNMjWTV4eNg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.4.5", + "aria-query": "^3.0.0", + "array-includes": "^3.0.3", + "ast-types-flow": "^0.0.7", + "axobject-query": "^2.0.2", + "damerau-levenshtein": "^1.0.4", + "emoji-regex": "^7.0.2", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6" + } + }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "dependencies": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz", + "integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz", + "integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.1", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.2.3", + "object.entries": "^1.1.1", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.15.1", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.2", + "xregexp": "^4.3.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", + "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", + "dev": true, + "engines": { + "node": ">=7" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/eslint/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", + "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "dev": true, + "dependencies": { + "original": "^1.0.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/expect/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/expect/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/expect/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/expect/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "dev": true, + "dependencies": { + "type": "^2.0.0" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "dependencies": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "dev": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "node_modules/filesize": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz", + "integrity": "sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", + "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", + "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "engines": { + "node": "*" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-3.1.1.tgz", + "integrity": "sha512-DuVkPNrM12jR41KM2e+N+styka0EgLkTnXmNcXdgOM37vtGeY+oCBK/Jx0hzSeEU6memFCtWb4htrHPMDfwwUQ==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.22.0", + "chalk": "^2.4.1", + "chokidar": "^3.3.0", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + }, + "engines": { + "node": ">=6.11.5", + "yarn": ">=1.0.0" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formik": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.6.tgz", + "integrity": "sha512-Kxk2zQRafy56zhLmrzcbryUpMBvT0tal5IvcifK5+4YNGelKsnrODFJ0sZQRMQboblWNym4lAW3bt+tf2vApSA==", + "funding": [ + { + "type": "individual", + "url": "https://opencollective.com/formik" + } + ], + "dependencies": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.14", + "lodash-es": "^4.17.14", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/formik/node_modules/react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, + "node_modules/formik/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dependencies": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/geojson-equality": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", + "integrity": "sha1-oXE3TvBD5dR5eZWEC65GSOB1LXI=", + "dependencies": { + "deep-equal": "^1.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true + }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", + "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "node_modules/globby/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globule": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", + "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", + "dependencies": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true + }, + "node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/gzip-size/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/harmony-reflect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.1.tgz", + "integrity": "sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-base/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/hash-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", + "dev": true + }, + "node_modules/hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "dev": true + }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^1.0.1" + } + }, + "node_modules/html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-webpack-plugin": { + "version": "4.0.0-beta.11", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.11.tgz", + "integrity": "sha512-4Xzepf0qWxf8CGg7/WQM5qBB2Lc/NFI7MhU59eUDTkuQp3skZczH4UA1d6oQyDEIoMDgERVhRyTdtUPZ5s5HBg==", + "deprecated": "please switch to a stable version", + "dev": true, + "dependencies": { + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.15", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + }, + "engines": { + "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/htmlparser2/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "dependencies": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "node_modules/hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=", + "dev": true, + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", + "dev": true + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, + "node_modules/immer": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", + "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==", + "dev": true + }, + "node_modules/import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=", + "dev": true, + "dependencies": { + "import-from": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha1-M1238qev/VOqpHHUuAId7ja387E=", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "dependencies": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/in-publish": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "bin": { + "in-install": "in-install.js", + "in-publish": "in-publish.js", + "not-in-install": "not-in-install.js", + "not-in-publish": "not-in-publish.js" + } + }, + "node_modules/indefinite-observable": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/indefinite-observable/-/indefinite-observable-2.0.1.tgz", + "integrity": "sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ==", + "dependencies": { + "symbol-observable": "1.2.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "dependencies": { + "type-fest": "^0.11.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "dependencies": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "dependencies": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internal-slot/node_modules/es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arguments": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dependencies": { + "call-bind": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "dev": true, + "dependencies": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=" + }, + "node_modules/is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "node_modules/is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-svg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-3.0.0.tgz", + "integrity": "sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==", + "dev": true, + "dependencies": { + "html-comment-regex": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "dependencies": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-changed-files/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-changed-files/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-changed-files/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-config/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-config/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "dependencies": { + "detect-newline": "^2.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-each/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom-fourteen": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom-fourteen/-/jest-environment-jsdom-fourteen-1.0.1.tgz", + "integrity": "sha512-DojMX1sY+at5Ep+O9yME34CdidZnO3/zfPh8UW+918C5fIZET5vCjfkegixmsi7AtdYfkr4bPlIzmWnlvQkP7Q==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.3.0", + "@jest/fake-timers": "^24.3.0", + "@jest/types": "^24.3.0", + "jest-mock": "^24.0.0", + "jest-util": "^24.0.0", + "jsdom": "^14.1.0" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/jsdom": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-14.1.0.tgz", + "integrity": "sha512-O901mfJSuTdwU2w3Sn+74T+RnDVP+FuV5fH8tcPWyqrseRAb0s5xOtPgCFiPOtLcyK7CLIJwPyD83ZqQWvA5ng==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "acorn": "^6.0.4", + "acorn-globals": "^4.3.0", + "array-equal": "^1.0.0", + "cssom": "^0.3.4", + "cssstyle": "^1.1.1", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.0", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.1.3", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.5", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.5.0", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^6.1.2", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/jest-environment-jsdom-fourteen/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-node/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-environment-node/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "engines": { + "node": ">= 6" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/jest-haste-map/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-haste-map/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-haste-map/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-jasmine2/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-jasmine2/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-jasmine2/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-jasmine2/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-jasmine2/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "dependencies": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-leak-detector/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-leak-detector/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-leak-detector/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-matcher-utils/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-matcher-utils/node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-message-util/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-message-util/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-mock/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-mock/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-resolve/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-resolve/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-runner/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest-runtime": "bin/jest-runtime.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runtime/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runtime/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-runtime/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-snapshot/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-sonar-reporter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz", + "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==", + "dev": true, + "dependencies": { + "xml": "^1.0.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-util/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-util/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-util/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-validate/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-validate/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-validate/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/jest-watch-typeahead": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-0.4.2.tgz", + "integrity": "sha512-f7VpLebTdaXs81rg/oj4Vg/ObZy2QtGzAmGLNsqUS5G5KtSN68tFcIsbvNODfNyQxU78g7D8x77o3bgfBTR+2Q==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.1", + "jest-regex-util": "^24.9.0", + "jest-watcher": "^24.3.0", + "slash": "^3.0.0", + "string-length": "^3.1.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "dependencies": { + "type-fest": "^0.11.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "dev": true, + "dependencies": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-watch-typeahead/node_modules/type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-watcher/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-watcher/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-watcher/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest/node_modules/@types/yargs": { + "version": "13.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.11.tgz", + "integrity": "sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest/node_modules/jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "dependencies": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==" + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "node_modules/jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "node_modules/json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "dependencies": { + "jsonify": "~0.0.0" + } + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "node_modules/json3": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", + "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "node_modules/jss": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.5.0.tgz", + "integrity": "sha512-B6151NvG+thUg3murLNHRPLxTLwQ13ep4SH5brj4d8qKtogOx/jupnpfkPGSHPqvcwKJaCLctpj2lEk+5yGwMw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "indefinite-observable": "^2.0.1", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.5.0.tgz", + "integrity": "sha512-GSjPL0adGAkuoqeYiXTgO7PlIrmjv5v8lA6TTBdfxbNYpxADOdGKJgIEkffhlyuIZHlPuuiFYTwUreLUmSn7rg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.5.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.5.0.tgz", + "integrity": "sha512-rsbTtZGCMrbcb9beiDd+TwL991NGmsAgVYH0hATrYJtue9e+LH/Gn4yFD1ENwE+3JzF3A+rPnM2JuD9L/SIIWw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.5.0.tgz", + "integrity": "sha512-FZd9+JE/3D7HMefEG54fEC0XiQ9rhGtDHAT/ols24y8sKQ1D5KIw6OyXEmIdKFmACgxZV2ARQ5pAUypxkk2IFQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.5.0.tgz", + "integrity": "sha512-ejPlCLNlEGgx8jmMiDk/zarsCZk+DV0YqXfddpgzbO9Toamo0HweCFuwJ3ZO40UFOfqKwfpKMVH/3HUXgxkTMg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.5.0.tgz", + "integrity": "sha512-kTLRvrOetFKz5vM88FAhLNeJIxfjhCepnvq65G7xsAQ/Wgy7HwO1BS/2wE5mx8iLaAWC6Rj5h16mhMk9sKdZxg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.5.0.tgz", + "integrity": "sha512-jXINGr8BSsB13JVuK274oEtk0LoooYSJqTBCGeBu2cG/VJ3+4FPs1gwLgsq24xTgKshtZ+WEQMVL34OprLidRA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.5.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.5.0.tgz", + "integrity": "sha512-rux3gmfwDdOKCLDx0IQjTwTm03IfBa+Rm/hs747cOw5Q7O3RaTUIMPKjtVfc31Xr/XI9Abz2XEupk1/oMQ7zRA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.5.0" + } + }, + "node_modules/jss/node_modules/csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + }, + "node_modules/jsx-ast-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", + "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/jszip": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-2.6.1.tgz", + "integrity": "sha1-uI86ey5noqBIFSmCx6N1bZxIKPA=", + "dependencies": { + "pako": "~1.0.2" + } + }, + "node_modules/keycloak-js": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-9.0.3.tgz", + "integrity": "sha512-c8FFPa8YiJmPbJEMZ/mIrHflBR6FIFUm5xTWtIDzlrnoeF4u0wDmTBfo1u71rWIL1HanLvg3T+9AgR1NqfmGbA==", + "dependencies": { + "base64-js": "1.3.1", + "js-sha256": "0.9.0" + } + }, + "node_modules/killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/last-call-webpack-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", + "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", + "dev": true, + "dependencies": { + "lodash": "^4.17.5", + "webpack-sources": "^1.1.0" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leaflet": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.7.1.tgz", + "integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==" + }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, + "node_modules/leaflet-fullscreen": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/leaflet-fullscreen/-/leaflet-fullscreen-1.0.2.tgz", + "integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs=" + }, + "node_modules/leaflet.locatecontrol": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.76.0.tgz", + "integrity": "sha512-Mx8uiihBi8KrrW3LgblsNL/pS8HR0gj60m8VFDFrnhSvDuitChazc095XcMSscf/XqZW+TSqQMCTe+AUy/4/eA==" + }, + "node_modules/leaflet.markercluster": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz", + "integrity": "sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA==", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "deprecated": "use String.prototype.padStart()", + "dev": true + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levenary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", + "integrity": "sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==", + "dev": true, + "dependencies": { + "leven": "^3.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-fs-cache": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz", + "integrity": "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^0.1.1", + "mkdirp": "^0.5.1" + } + }, + "node_modules/loader-fs-cache/node_modules/find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-fs-cache/node_modules/pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", + "dev": true, + "dependencies": { + "find-up": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "node_modules/loglevel": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lz-string": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "dependencies": { + "tmpl": "1.0.x" + } + }, + "node_modules/mamacro": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", + "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", + "dev": true + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, + "node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dependencies": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow/node_modules/indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow/node_modules/redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dependencies": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/meow/node_modules/strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dependencies": { + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mgrs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", + "integrity": "sha1-+5FYjnjJACVnI5XLQLJffNatGCk=" + }, + "node_modules/microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dependencies": { + "mime-db": "1.44.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-create-react-context": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.3.tgz", + "integrity": "sha512-TtF6hZE59SGmS4U8529qB+jJFeW6asTLDIpPgvPLSCsooAwJS7QprHIFTqv9/Qh3NdLwQxFYgiHX5lqb6jqzPA==", + "dependencies": { + "@babel/runtime": "^7.12.1", + "tiny-warning": "^1.0.3" + }, + "peerDependencies": { + "prop-types": "^15.0.0", + "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.4.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "dev": true, + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", + "engines": { + "node": "*" + } + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" + }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dependencies": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "node_modules/node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-notifier": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", + "dev": true, + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "node_modules/node-releases": { + "version": "1.1.70", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", + "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==", + "dev": true + }, + "node_modules/node-sass": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", + "hasInstallScript": true, + "dependencies": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "2.2.5", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "bin": { + "node-sass": "bin/node-sass" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-sass/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-sass/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-sass/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dependencies": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", + "dev": true + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz", + "integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.3.tgz", + "integrity": "sha512-ym7h7OZebNS96hn5IJeyUmaWhaSM4SVtAPPfNLQEI2MYWCO2egsITb9nab2+i/Pwibx+R0mtn+ltKJXRSeTMGg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.3.tgz", + "integrity": "sha512-IDUSMXs6LOSJBWE++L0lzIbSqHl9KDCfff2x/JSEIDtEUavUnyMYC2ZGay/04Zq4UT8lvd4xNhU4/YHKibAOlw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", + "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/open/-/open-7.3.1.tgz", + "integrity": "sha512-f2wt9DCBKKjlFbjzGb8MOAW8LH8F0mrs1zc7KTjAJ9PZNQbfenzWbNP1VZJvw6ICMG9r14Ah6yfwPn7T7i646A==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/optimize-css-assets-webpack-plugin": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.3.tgz", + "integrity": "sha512-q9fbvCRS6EYtUKKSwI87qm2IxlyJK5b4dygW1rKUBT6mMDhdG5e5bZT63v6tnJR9F9FB/H5a0HTmtw+laUBxKA==", + "dev": true, + "dependencies": { + "cssnano": "^4.1.10", + "last-call-webpack-plugin": "^3.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dev": true, + "dependencies": { + "url-parse": "^1.4.3" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", + "dev": true, + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "dependencies": { + "retry": "^0.12.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "dev": true, + "dependencies": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "node_modules/parsedbf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parsedbf/-/parsedbf-1.1.1.tgz", + "integrity": "sha512-jndFmhcrzSAGCMccM4za+3bIRxqV6L2doQjYN8Xgz0kZUpyBT5I8Gs6Y6hL5GcO2rih9OBkPcLlx2uBoLi8R8Q==", + "dependencies": { + "iconv-lite": "^0.4.15", + "text-encoding-polyfill": "^0.6.7" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "node_modules/picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "dependencies": { + "node-modules-regexp": "^1.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "node_modules/pnp-webpack-plugin": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz", + "integrity": "sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==", + "dev": true, + "dependencies": { + "ts-pnp": "^1.1.6" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" + }, + "node_modules/portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "dependencies": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz", + "integrity": "sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^6.0.2" + } + }, + "node_modules/postcss-browser-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-3.0.0.tgz", + "integrity": "sha512-qfVjLfq7HFd2e0HW4s1dvU8X080OZdG46fFbIBFjW7US7YPDcWfRvdElvwMJr2LI6hMmD+7LnH2HcmXTs+uOig==", + "dev": true, + "dependencies": { + "postcss": "^7" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "browserslist": "^4" + } + }, + "node_modules/postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz", + "integrity": "sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-color-gray": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz", + "integrity": "sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==", + "dev": true, + "dependencies": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz", + "integrity": "sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14", + "postcss-values-parser": "^2.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-color-mod-function": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz", + "integrity": "sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==", + "dev": true, + "dependencies": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz", + "integrity": "sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-colormin/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-custom-media": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz", + "integrity": "sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-custom-properties": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz", + "integrity": "sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.17", + "postcss-values-parser": "^2.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz", + "integrity": "sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-custom-selectors/node_modules/cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "dependencies": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz", + "integrity": "sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "dependencies": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz", + "integrity": "sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-env-function": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-2.0.2.tgz", + "integrity": "sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-flexbugs-fixes": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz", + "integrity": "sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + } + }, + "node_modules/postcss-focus-visible": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz", + "integrity": "sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-focus-within": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz", + "integrity": "sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-font-variant": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz", + "integrity": "sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-gap-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz", + "integrity": "sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-image-set-function": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz", + "integrity": "sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-3.0.2.tgz", + "integrity": "sha512-ugA2wKonC0xeNHgirR4D3VWHs2JcU08WAi1KFLVcnb7IN89phID6Qtg2RIctWbnvp1TM2BOmDtX8GGLCKdR8YA==", + "dev": true, + "dependencies": { + "lodash.template": "^4.5.0", + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-lab-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz", + "integrity": "sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==", + "dev": true, + "dependencies": { + "@csstools/convert-colors": "^1.4.0", + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-load-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/postcss-logical": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-3.0.0.tgz", + "integrity": "sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-media-minmax": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz", + "integrity": "sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "dependencies": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "dependencies": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "dependencies": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "node_modules/postcss-nesting": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-7.0.1.tgz", + "integrity": "sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-normalize": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-8.0.1.tgz", + "integrity": "sha512-rt9JMS/m9FHIRroDDBGSMsyW1c0fkvOJPy62ggxSHUldJO7B195TqFMqIf+lY5ezpDcYOV4j86aUp3/XbxzCCQ==", + "dev": true, + "dependencies": { + "@csstools/normalize.css": "^10.1.0", + "browserslist": "^4.6.2", + "postcss": "^7.0.17", + "postcss-browser-comments": "^3.0.0", + "sanitize.css": "^10.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "dependencies": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "dependencies": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-url/node_modules/normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-overflow-shorthand": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz", + "integrity": "sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-page-break": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-2.0.0.tgz", + "integrity": "sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-place": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-4.0.1.tgz", + "integrity": "sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-values-parser": "^2.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-preset-env": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz", + "integrity": "sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg==", + "dev": true, + "dependencies": { + "autoprefixer": "^9.6.1", + "browserslist": "^4.6.4", + "caniuse-lite": "^1.0.30000981", + "css-blank-pseudo": "^0.1.4", + "css-has-pseudo": "^0.10.0", + "css-prefers-color-scheme": "^3.1.1", + "cssdb": "^4.4.0", + "postcss": "^7.0.17", + "postcss-attribute-case-insensitive": "^4.0.1", + "postcss-color-functional-notation": "^2.0.1", + "postcss-color-gray": "^5.0.0", + "postcss-color-hex-alpha": "^5.0.3", + "postcss-color-mod-function": "^3.0.3", + "postcss-color-rebeccapurple": "^4.0.1", + "postcss-custom-media": "^7.0.8", + "postcss-custom-properties": "^8.0.11", + "postcss-custom-selectors": "^5.1.2", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-double-position-gradients": "^1.0.0", + "postcss-env-function": "^2.0.2", + "postcss-focus-visible": "^4.0.0", + "postcss-focus-within": "^3.0.0", + "postcss-font-variant": "^4.0.0", + "postcss-gap-properties": "^2.0.0", + "postcss-image-set-function": "^3.0.1", + "postcss-initial": "^3.0.0", + "postcss-lab-function": "^2.0.1", + "postcss-logical": "^3.0.0", + "postcss-media-minmax": "^4.0.0", + "postcss-nesting": "^7.0.0", + "postcss-overflow-shorthand": "^2.0.0", + "postcss-page-break": "^2.0.0", + "postcss-place": "^4.0.1", + "postcss-pseudo-class-any-link": "^6.0.0", + "postcss-replace-overflow-wrap": "^3.0.0", + "postcss-selector-matches": "^4.0.0", + "postcss-selector-not": "^4.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz", + "integrity": "sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2", + "postcss-selector-parser": "^5.0.0-rc.3" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/cssesc": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-2.0.0.tgz", + "integrity": "sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz", + "integrity": "sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==", + "dev": true, + "dependencies": { + "cssesc": "^2.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz", + "integrity": "sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-safe-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz", + "integrity": "sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-selector-matches": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz", + "integrity": "sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-selector-not": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz", + "integrity": "sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "postcss": "^7.0.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.2.tgz", + "integrity": "sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==", + "dev": true, + "dependencies": { + "is-svg": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-svgo/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "node_modules/postcss-values-parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz", + "integrity": "sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==", + "dev": true, + "dependencies": { + "flatten": "^1.0.2", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=6.14.4" + } + }, + "node_modules/postcss/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", + "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", + "dev": true, + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9" + } + }, + "node_modules/pretty-bytes": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz", + "integrity": "sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proj4": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.7.2.tgz", + "integrity": "sha512-x/EboBmIq48a9FED0Z9zWCXkd8VIpXHLsyEXljGtsnzeztC41bFjPjJ0S//wBbNLDnDYRe0e6c3FSSiqMCebDA==", + "dependencies": { + "mgrs": "1.0.0", + "wkt-parser": "^1.2.4" + } + }, + "node_modules/promise": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", + "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", + "dev": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-app-polyfill": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz", + "integrity": "sha512-OfBnObtnGgLGfweORmdZbyEz+3dgVePQBb3zipiaDsMHV1NpWm0rDFYIVXFV/AK+x4VIIfWHhrdMIeoTLyRr2g==", + "dev": true, + "dependencies": { + "core-js": "^3.5.0", + "object-assign": "^4.1.1", + "promise": "^8.0.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.3", + "whatwg-fetch": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-app-polyfill/node_modules/core-js": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz", + "integrity": "sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/react-dev-utils": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", + "integrity": "sha512-XxTbgJnYZmxuPtY3y/UV0D8/65NKkmaia4rXzViknVnZeVlklSh8u6TnaEYPfAi/Gh1TP4mEOXHI6jQOPbeakQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.8.3", + "address": "1.1.2", + "browserslist": "4.10.0", + "chalk": "2.4.2", + "cross-spawn": "7.0.1", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "2.0.0", + "filesize": "6.0.1", + "find-up": "4.1.0", + "fork-ts-checker-webpack-plugin": "3.1.1", + "global-modules": "2.0.0", + "globby": "8.0.2", + "gzip-size": "5.1.1", + "immer": "1.10.0", + "inquirer": "7.0.4", + "is-root": "2.1.0", + "loader-utils": "1.2.3", + "open": "^7.0.2", + "pkg-up": "3.1.0", + "react-error-overlay": "^6.0.7", + "recursive-readdir": "2.2.2", + "shell-quote": "1.7.2", + "strip-ansi": "6.0.0", + "text-table": "0.2.0" + }, + "engines": { + "node": ">=8.10" + } + }, + "node_modules/react-dev-utils/node_modules/@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.8.3" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "dependencies": { + "type-fest": "^0.11.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/browserslist": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.10.0.tgz", + "integrity": "sha512-TpfK0TDgv71dzuTsEAlQiHeWQ/tiPqgNZVdv046fvNtBZrjbv2O3TsWCDU0AWGJJKCF/KsjNdLzR9hXOsh/CfA==", + "dev": true, + "dependencies": { + "caniuse-lite": "^1.0.30001035", + "electron-to-chromium": "^1.3.378", + "node-releases": "^1.1.52", + "pkg-up": "^3.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + }, + "node_modules/react-dev-utils/node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "node_modules/react-dev-utils/node_modules/cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/react-dev-utils/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/react-dev-utils/node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react-dev-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/inquirer": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", + "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/inquirer/node_modules/ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-dev-utils/node_modules/inquirer/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-dev-utils/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/react-dev-utils/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/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, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/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==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-dev-utils/node_modules/type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-dev-utils/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-dropzone": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.3.2.tgz", + "integrity": "sha512-Z0l/YHcrNK1r85o6RT77Z5XgTARmlZZGfEKBl3tqTXL9fZNQDuIdRx/J0QjvR60X+yYu26dnHeaG2pWU+1HHvw==", + "dependencies": { + "attr-accept": "^2.2.1", + "file-selector": "^0.2.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 16.8" + } + }, + "node_modules/react-dropzone/node_modules/file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.8.tgz", + "integrity": "sha512-HvPuUQnLp5H7TouGq3kzBeioJmXms1wHy9EGjz2OURWBp4qZO6AfGEcnxts1D/CbwPLRAgTMPCEgYhA3sEM4vw==", + "dev": true + }, + "node_modules/react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + }, + "node_modules/react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==" + }, + "node_modules/react-leaflet": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-3.1.0.tgz", + "integrity": "sha512-kdZS8NYbYFPmkQr7zSDR2gkKGFeWvkxqoqcmZEckzHL4d5c85dJ2gbbqhaPDpmWWgaRw9O29uA/77qpKmK4mTQ==", + "dependencies": { + "@react-leaflet/core": "^1.0.2" + }, + "peerDependencies": { + "leaflet": "^1.7.1", + "react": "^17.0.1", + "react-dom": "^17.0.1" + } + }, + "node_modules/react-leaflet-cluster": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-leaflet-cluster/-/react-leaflet-cluster-1.0.3.tgz", + "integrity": "sha512-CzYeMPfSyAn8fWNEhhk/NIYGZ0sFAyMygTuXeNMcdgNKZOJHWyqrKg0GrAxniWz/koTJpXYv1B1lU1JURF+x5A==", + "dependencies": { + "leaflet.markercluster": "^1.4.1" + }, + "peerDependencies": { + "leaflet": "^1.7.1", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-leaflet": "^3.0.2" + } + }, + "node_modules/react-number-format": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-4.5.2.tgz", + "integrity": "sha512-5pE0q6Q3pw0phFPpKWjyqRPCaajroggB8L43goMQzBEFyAi7LF+OpZH+uB9Wxg+rTOt/Ydi5H5Q2O5pmHvGzrg==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.14 || ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0", + "react-dom": "^0.14 || ^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0" + } + }, + "node_modules/react-router": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", + "integrity": "sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "mini-create-react-context": "^0.3.0", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-dom": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz", + "integrity": "sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.1.2", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/react-router/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/react-router/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-scripts": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.4.tgz", + "integrity": "sha512-7J7GZyF/QvZkKAZLneiOIhHozvOMHey7hO9cdO9u68jjhGZlI8hDdOm6UyuHofn6Ajc9Uji5I6Psm/nKNuWdyw==", + "dev": true, + "dependencies": { + "@babel/core": "7.9.0", + "@svgr/webpack": "4.3.3", + "@typescript-eslint/eslint-plugin": "^2.10.0", + "@typescript-eslint/parser": "^2.10.0", + "babel-eslint": "10.1.0", + "babel-jest": "^24.9.0", + "babel-loader": "8.1.0", + "babel-plugin-named-asset-import": "^0.3.6", + "babel-preset-react-app": "^9.1.2", + "camelcase": "^5.3.1", + "case-sensitive-paths-webpack-plugin": "2.3.0", + "css-loader": "3.4.2", + "dotenv": "8.2.0", + "dotenv-expand": "5.1.0", + "eslint": "^6.6.0", + "eslint-config-react-app": "^5.2.1", + "eslint-loader": "3.0.3", + "eslint-plugin-flowtype": "4.6.0", + "eslint-plugin-import": "2.20.1", + "eslint-plugin-jsx-a11y": "6.2.3", + "eslint-plugin-react": "7.19.0", + "eslint-plugin-react-hooks": "^1.6.1", + "file-loader": "4.3.0", + "fs-extra": "^8.1.0", + "html-webpack-plugin": "4.0.0-beta.11", + "identity-obj-proxy": "3.0.0", + "jest": "24.9.0", + "jest-environment-jsdom-fourteen": "1.0.1", + "jest-resolve": "24.9.0", + "jest-watch-typeahead": "0.4.2", + "mini-css-extract-plugin": "0.9.0", + "optimize-css-assets-webpack-plugin": "5.0.3", + "pnp-webpack-plugin": "1.6.4", + "postcss-flexbugs-fixes": "4.1.0", + "postcss-loader": "3.0.0", + "postcss-normalize": "8.0.1", + "postcss-preset-env": "6.7.0", + "postcss-safe-parser": "4.0.1", + "react-app-polyfill": "^1.0.6", + "react-dev-utils": "^10.2.1", + "resolve": "1.15.0", + "resolve-url-loader": "3.1.2", + "sass-loader": "8.0.2", + "semver": "6.3.0", + "style-loader": "0.23.1", + "terser-webpack-plugin": "2.3.8", + "ts-pnp": "1.1.6", + "url-loader": "2.3.0", + "webpack": "4.42.0", + "webpack-dev-server": "3.11.0", + "webpack-manifest-plugin": "2.2.0", + "workbox-webpack-plugin": "4.3.1" + }, + "bin": { + "react-scripts": "bin/react-scripts.js" + }, + "engines": { + "node": ">=8.10" + }, + "optionalDependencies": { + "fsevents": "2.1.2" + }, + "peerDependencies": { + "typescript": "^3.2.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/@babel/core": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.9.0.tgz", + "integrity": "sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.9.0", + "@babel/helper-module-transforms": "^7.9.0", + "@babel/helpers": "^7.9.0", + "@babel/parser": "^7.9.0", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.9.0", + "@babel/types": "^7.9.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/react-scripts/node_modules/@babel/core/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/eslint-plugin": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "2.34.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^2.0.0", + "eslint": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/eslint-plugin/node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/parser": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "dev": true, + "dependencies": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-scripts/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/react-scripts/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scripts/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/react-scripts/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/react-scripts/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/react-scripts/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scripts/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-scripts/node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-scripts/node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-scripts/node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/react-scripts/node_modules/fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "deprecated": "\"Please update to latest v2.3 or v2.2\"", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/react-scripts/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-scripts/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-scripts/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-scripts/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/react-scripts/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/react-scripts/node_modules/resolve": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", + "integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==", + "dev": true, + "dependencies": { + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/react-scripts/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/react-scripts/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/react-scripts/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/react-scripts/node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scripts/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scripts/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-scripts/node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-window": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz", + "integrity": "sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "dependencies": { + "util.promisify": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "dev": true, + "dependencies": { + "minimatch": "3.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", + "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + }, + "node_modules/regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.6.tgz", + "integrity": "sha512-jjyuCp+IEMIm3N1H1LLTJW1EISEJV9+5oHdEyrt43Pg9cDSb6rrLZei2cVWpl0xTjmmlpec/lEQGYgM7xfpGCQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "node_modules/renderkid": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", + "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "dev": true, + "dependencies": { + "css-select": "^2.0.2", + "dom-converter": "^0.2", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.20", + "strip-ansi": "^3.0.0" + } + }, + "node_modules/repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dependencies": { + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reproj-helper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/reproj-helper/-/reproj-helper-1.2.8.tgz", + "integrity": "sha512-Hj9BP85FnaF1QXCZqHmK3ISSDiXI4TD2lNzo7HyZqEENKNAT32qwr1ZPXVkQoTpXb++vw7P753fxeEdUPsa8fQ==", + "dependencies": { + "proj4": "^2.6.3", + "ts-deepcopy": "^0.1.4" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/resolve-url-loader": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.2.tgz", + "integrity": "sha512-QEb4A76c8Mi7I3xNKXlRKQSlLBwjUV/ULFMP+G7n3/7tJZ8MG5wsZ3ucxP1Jz8Vevn6fnJsxDx9cIls+utGzPQ==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "3.0.0", + "camelcase": "5.3.1", + "compose-function": "3.0.3", + "convert-source-map": "1.7.0", + "es6-iterator": "2.0.3", + "loader-utils": "1.2.3", + "postcss": "7.0.21", + "rework": "1.0.1", + "rework-visit": "1.0.0", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/resolve-url-loader/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/resolve-url-loader/node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-url-loader/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/resolve-url-loader/node_modules/postcss": { + "version": "7.0.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.21.tgz", + "integrity": "sha512-uIFtJElxJo29QC753JzhidoAhvp/e/Exezkdhfmt8AymWT6/5B7W1WmponYWkHk2eg6sONyTch0A3nkMPun3SQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-url-loader/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rework": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", + "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", + "dev": true, + "dependencies": { + "convert-source-map": "^0.3.3", + "css": "^2.0.0" + } + }, + "node_modules/rework-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", + "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=", + "dev": true + }, + "node_modules/rework/node_modules/convert-source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", + "dev": true + }, + "node_modules/rework/node_modules/css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "node_modules/rework/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rework/node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", + "dev": true + }, + "node_modules/rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", + "dev": true + }, + "node_modules/rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "dependencies": { + "@babel/runtime": "^7.3.1" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "dev": true, + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", + "dev": true, + "dependencies": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "bin": { + "sane": "src/cli.js" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/sanitize.css": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/sanitize.css/-/sanitize.css-10.0.0.tgz", + "integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg==", + "dev": true + }, + "node_modules/sass-graph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "dependencies": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^13.3.2" + }, + "bin": { + "sassgraph": "bin/sassgraph" + } + }, + "node_modules/sass-loader": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-8.0.2.tgz", + "integrity": "sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.2.3", + "neo-async": "^2.6.1", + "schema-utils": "^2.6.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0", + "sass": "^1.3.0", + "webpack": "^4.36.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/sass-loader/node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/sass-loader/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sass-loader/node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "dependencies": { + "xmlchars": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dependencies": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + } + }, + "node_modules/scss-tokenizer/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "node_modules/selfsigned": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "dev": true, + "dependencies": { + "node-forge": "^0.10.0" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", + "dev": true, + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "node_modules/shpjs": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/shpjs/-/shpjs-3.6.3.tgz", + "integrity": "sha512-wcR2S3WL/7RnEIm+YO+H/mZR9z9FCV46op+SZt+W5PtPs26Omb9U93f+EPI1DOpNKBuAIrWjHWh0SxlnBahJkg==", + "dependencies": { + "jszip": "^2.4.0", + "lie": "^3.0.1", + "lru-cache": "^2.7.0", + "parsedbf": "^1.1.0", + "proj4": "^2.1.4" + } + }, + "node_modules/shpjs/node_modules/lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.10.0", + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" + } + }, + "node_modules/sockjs-client": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", + "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "dev": true, + "dependencies": { + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sockjs-client/node_modules/faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sockjs-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz", + "integrity": "sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw==", + "dev": true, + "dependencies": { + "figgy-pudding": "^3.5.1", + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.4.tgz", + "integrity": "sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", + "dev": true + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", + "dev": true, + "dependencies": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.3.tgz", + "integrity": "sha512-OBxYDA2ifZQ2e13cP82dWFMaCV9CGF8GzmN4fljBVw5O5wep0lu4gacm1OL6MjROoUnB8VbkWRThqkV2YFLNxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/stringify-object/node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-comments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz", + "integrity": "sha512-kL97alc47hoyIQSV165tTt9rG5dn4w1dNnBhOQ3bOU1Nc1hel09jnXANaHJ7vzHLd4Ju8kseDGzlev96pghLFw==", + "dev": true, + "dependencies": { + "babel-extract-comments": "^1.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", + "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/style-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/stylehacks/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "dev": true, + "dependencies": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo/node_modules/util.promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.", + "dependencies": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "node_modules/terser": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.8.tgz", + "integrity": "sha512-/fKw3R+hWyHfYx7Bv6oPqmk4HGQcrWLtV3X6ggvPuwPNHSnzvVV51z6OaaCOus4YLjutYGOz3pEpbhe6Up2s1w==", + "dev": true, + "dependencies": { + "cacache": "^13.0.1", + "find-cache-dir": "^3.3.1", + "jest-worker": "^25.4.0", + "p-limit": "^2.3.0", + "schema-utils": "^2.6.6", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.6.12", + "webpack-sources": "^1.4.3" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "dev": true, + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 8.3" + } + }, + "node_modules/terser-webpack-plugin/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser-webpack-plugin/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude/node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-encoding-polyfill": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/text-encoding-polyfill/-/text-encoding-polyfill-0.6.7.tgz", + "integrity": "sha512-/DZ1XJqhbqRkCop6s9ZFu8JrFRwmVuHg4quIRm+ziFkR3N3ec6ck6yBvJ1GYeEQZhLVwRW0rZE+C3SSJpy0RTg==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz", + "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dependencies": { + "glob": "^7.1.2" + } + }, + "node_modules/ts-deepcopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ts-deepcopy/-/ts-deepcopy-0.1.4.tgz", + "integrity": "sha512-F7ssOS1Se4OKboKPCkCK3aJlC+mSflN00Rh1fMImNYPM288pP0xEcDH172t3457OrymLwTs4A2N56BsVaEAVmA==" + }, + "node_modules/ts-pnp": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.1.6.tgz", + "integrity": "sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", + "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "node_modules/typescript": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", + "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", + "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^1.0.4", + "unicode-property-aliases-ecmascript": "^1.0.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", + "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", + "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, + "node_modules/uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "dev": true + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", + "dev": true + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", + "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", + "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/mime": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", + "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/util.promisify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", + "integrity": "sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "for-each": "^0.3.3", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz", + "integrity": "sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "dependencies": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "dependencies": { + "makeerror": "1.0.x" + } + }, + "node_modules/watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, + "node_modules/watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "dependencies": { + "chokidar": "^2.1.8" + } + }, + "node_modules/watchpack-chokidar2/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "optional": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/webpack": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.42.0.tgz", + "integrity": "sha512-EzJRHvwQyBiYrYqhyjW9AqM90dE4+s1/XtCfn7uWg6cS72zH+2VPFAlsnW0+W0cDi0XRjNKUMoJtpSi50+Ph6w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.8.5", + "@webassemblyjs/helper-module-context": "1.8.5", + "@webassemblyjs/wasm-edit": "1.8.5", + "@webassemblyjs/wasm-parser": "1.8.5", + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "dependencies": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", + "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack-dev-server": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "dev": true, + "dependencies": { + "ansi-html": "0.0.7", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.20", + "sockjs-client": "1.4.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 6.11.5" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/webpack-dev-server/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack-dev-server/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/webpack-dev-server/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-log/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/webpack-manifest-plugin": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-2.2.0.tgz", + "integrity": "sha512-9S6YyKKKh/Oz/eryM1RyLVDVmy3NSPV0JXMRhZ18fJsq+AwGxUY34X54VNwkzYcEmEkDwNxuEOboCZEebJXBAQ==", + "dev": true, + "dependencies": { + "fs-extra": "^7.0.0", + "lodash": ">=3.5 <5", + "object.entries": "^1.1.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.11.5" + }, + "peerDependencies": { + "webpack": "2 || 3 || 4" + } + }, + "node_modules/webpack-manifest-plugin/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack/node_modules/ssri": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "dev": true, + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/webpack/node_modules/terser-webpack-plugin": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", + "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "dev": true, + "dependencies": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/webpack/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/websocket-driver": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "dev": true, + "dependencies": { + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz", + "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A==", + "dev": true + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wkt-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.2.4.tgz", + "integrity": "sha512-ZzKnc7ml/91fOPh5bANBL4vUlWPIYYv11waCtWTkl2TRN+LEmBg60Q1MA8gqV4hEp4MGfSj9JiHz91zw/gTDXg==" + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-background-sync": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-4.3.1.tgz", + "integrity": "sha512-1uFkvU8JXi7L7fCHVBEEnc3asPpiAL33kO495UMcD5+arew9IbKW2rV5lpzhoWcm/qhGB89YfO4PmB/0hQwPRg==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-4.3.1.tgz", + "integrity": "sha512-MTSfgzIljpKLTBPROo4IpKjESD86pPFlZwlvVG32Kb70hW+aob4Jxpblud8EhNb1/L5m43DUM4q7C+W6eQMMbA==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-build": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-4.3.1.tgz", + "integrity": "sha512-UHdwrN3FrDvicM3AqJS/J07X0KXj67R8Cg0waq1MKEOqzo89ap6zh6LmaLnRAjpB+bDIz+7OlPye9iii9KBnxw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.3.4", + "@hapi/joi": "^15.0.0", + "common-tags": "^1.8.0", + "fs-extra": "^4.0.2", + "glob": "^7.1.3", + "lodash.template": "^4.4.0", + "pretty-bytes": "^5.1.0", + "stringify-object": "^3.3.0", + "strip-comments": "^1.0.2", + "workbox-background-sync": "^4.3.1", + "workbox-broadcast-update": "^4.3.1", + "workbox-cacheable-response": "^4.3.1", + "workbox-core": "^4.3.1", + "workbox-expiration": "^4.3.1", + "workbox-google-analytics": "^4.3.1", + "workbox-navigation-preload": "^4.3.1", + "workbox-precaching": "^4.3.1", + "workbox-range-requests": "^4.3.1", + "workbox-routing": "^4.3.1", + "workbox-strategies": "^4.3.1", + "workbox-streams": "^4.3.1", + "workbox-sw": "^4.3.1", + "workbox-window": "^4.3.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/workbox-build/node_modules/fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-4.3.1.tgz", + "integrity": "sha512-Rp5qlzm6z8IOvnQNkCdO9qrDgDpoPNguovs0H8C+wswLuPgSzSp9p2afb5maUt9R1uTIwOXrVQMmPfPypv+npw==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-core": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-4.3.1.tgz", + "integrity": "sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg==", + "dev": true + }, + "node_modules/workbox-expiration": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-4.3.1.tgz", + "integrity": "sha512-vsJLhgQsQouv9m0rpbXubT5jw0jMQdjpkum0uT+d9tTwhXcEZks7qLfQ9dGSaufTD2eimxbUOJfWLbNQpIDMPw==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz", + "integrity": "sha512-xzCjAoKuOb55CBSwQrbyWBKqp35yg1vw9ohIlU2wTy06ZrYfJ8rKochb1MSGlnoBfXGWss3UPzxR5QL5guIFdg==", + "dev": true, + "dependencies": { + "workbox-background-sync": "^4.3.1", + "workbox-core": "^4.3.1", + "workbox-routing": "^4.3.1", + "workbox-strategies": "^4.3.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-4.3.1.tgz", + "integrity": "sha512-K076n3oFHYp16/C+F8CwrRqD25GitA6Rkd6+qAmLmMv1QHPI2jfDwYqrytOfKfYq42bYtW8Pr21ejZX7GvALOw==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-precaching": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-4.3.1.tgz", + "integrity": "sha512-piSg/2csPoIi/vPpp48t1q5JLYjMkmg5gsXBQkh/QYapCdVwwmKlU9mHdmy52KsDGIjVaqEUMFvEzn2LRaigqQ==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz", + "integrity": "sha512-S+HhL9+iTFypJZ/yQSl/x2Bf5pWnbXdd3j57xnb0V60FW1LVn9LRZkPtneODklzYuFZv7qK6riZ5BNyc0R0jZA==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-routing": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-4.3.1.tgz", + "integrity": "sha512-FkbtrODA4Imsi0p7TW9u9MXuQ5P4pVs1sWHK4dJMMChVROsbEltuE79fBoIk/BCztvOJ7yUpErMKa4z3uQLX+g==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-strategies": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-4.3.1.tgz", + "integrity": "sha512-F/+E57BmVG8dX6dCCopBlkDvvhg/zj6VDs0PigYwSN23L8hseSRwljrceU2WzTvk/+BSYICsWmRq5qHS2UYzhw==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-streams": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-4.3.1.tgz", + "integrity": "sha512-4Kisis1f/y0ihf4l3u/+ndMkJkIT4/6UOacU3A4BwZSAC9pQ9vSvJpIi/WFGQRH/uPXvuVjF5c2RfIPQFSS2uA==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/workbox-sw": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-4.3.1.tgz", + "integrity": "sha512-0jXdusCL2uC5gM3yYFT6QMBzKfBr2XTk0g5TPAV4y8IZDyVNDyj1a8uSXy3/XrvkVTmQvLN4O5k3JawGReXr9w==", + "dev": true + }, + "node_modules/workbox-webpack-plugin": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-4.3.1.tgz", + "integrity": "sha512-gJ9jd8Mb8wHLbRz9ZvGN57IAmknOipD3W4XNE/Lk/4lqs5Htw4WOQgakQy/o/4CoXQlMCYldaqUg+EJ35l9MEQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "json-stable-stringify": "^1.0.1", + "workbox-build": "^4.3.1" + }, + "engines": { + "node": ">=4.0.0" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/workbox-window": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-4.3.1.tgz", + "integrity": "sha512-C5gWKh6I58w3GeSc0wp2Ne+rqVw8qwcmZnQGpjiek8A2wpbxSJb1FdCoQVO+jDJs35bFgo/WETgl1fqgsxN0Hg==", + "dev": true, + "dependencies": { + "workbox-core": "^4.3.1" + } + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "dev": true, + "dependencies": { + "microevent.ts": "~0.1.1" + } + }, + "node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xregexp": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.1.tgz", + "integrity": "sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==", + "dev": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.12.1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "engines": { + "node": ">=4" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yup": { + "version": "0.32.9", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz", + "integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==", + "dependencies": { + "@babel/runtime": "^7.10.5", + "@types/lodash": "^4.14.165", + "lodash": "^4.17.20", + "lodash-es": "^4.17.15", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } + } + }, "dependencies": { "@babel/code-frame": { "version": "7.12.11", @@ -1826,7 +24173,8 @@ "@material-ui/types": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", - "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==" + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "requires": {} }, "@material-ui/utils": { "version": "4.11.2", @@ -1839,9 +24187,9 @@ } }, "@mdi/js": { - "version": "5.9.55", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-5.9.55.tgz", - "integrity": "sha512-BbeHMgeK2/vjdJIRnx12wvQ6s8xAYfvMmEAVsUx9b+7GiQGQ9Za8jpwp17dMKr9CgKRvemlAM4S7S3QOtEbp4A==" + "version": "6.4.95", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.4.95.tgz", + "integrity": "sha512-b1/P//1D2KOzta8YRGyoSLGsAlWyUHfxzVBhV4e/ppnjM4DfBgay/vWz7Eg5Ee80JZ4zsQz8h54X+KOahtBk5Q==" }, "@mdi/react": { "version": "1.4.0", @@ -1888,7 +24236,8 @@ "@react-leaflet/core": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-1.0.2.tgz", - "integrity": "sha512-QbleYZTMcgujAEyWGki8Lx6cXQqWkNtQlqf5c7NImlIp8bKW66bFpez/6EVatW7+p9WKBOEOVci/9W7WW70EZg==" + "integrity": "sha512-QbleYZTMcgujAEyWGki8Lx6cXQqWkNtQlqf5c7NImlIp8bKW66bFpez/6EVatW7+p9WKBOEOVci/9W7WW70EZg==", + "requires": {} }, "@svgr/babel-plugin-add-jsx-attribute": { "version": "4.2.0", @@ -2378,9 +24727,9 @@ } }, "@types/leaflet-fullscreen": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/leaflet-fullscreen/-/leaflet-fullscreen-1.0.5.tgz", - "integrity": "sha512-c27EGPkzXanBUFYPB3w/zcAf3+Qk6kWe4dgNKaEElnMz1HAymuK9bS/D7mR/m6WkdtkAQGP6MM9/fF6fLTeCvA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/leaflet-fullscreen/-/leaflet-fullscreen-1.0.6.tgz", + "integrity": "sha512-Kd0T+YDJgtiY02iwjbt2zntGdzGZ+/4MspAJchu5WXz1uoE4EE1K4zmVAOjKFTX/fwzA6OTZBmefVyE3H+HSYg==", "dev": true, "requires": { "@types/leaflet": "*" @@ -2407,11 +24756,20 @@ "dev": true }, "@types/node": { - "version": "12.12.70", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.70.tgz", - "integrity": "sha512-i5y7HTbvhonZQE+GnUM2rz1Bi8QkzxdQmEv1LKOv4nWyaQk/gdeiTApuQR3PDJHX7WomAbpx2wlWSEpxXGZ/UQ==", + "version": "14.14.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.45.tgz", + "integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==", "dev": true }, + "@types/node-sass": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@types/node-sass/-/node-sass-4.11.2.tgz", + "integrity": "sha512-pOFlTw/OtZda4e+yMjq6/QYuvY0RDMQ+mxXdWj7rfSyf18V8hS4SfgurO+MasAkQsv6Wt6edOGlwh5QqJml9gw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -2563,49 +24921,86 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", - "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", + "semver": "^7.3.2", "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, "@typescript-eslint/experimental-utils": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", - "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.34.0", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", - "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.34.0", - "@typescript-eslint/typescript-estree": "2.34.0", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", "eslint-visitor-keys": "^1.1.0" } }, + "@typescript-eslint/types": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "dev": true + }, "@typescript-eslint/typescript-estree": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", - "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, "requires": { + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", "debug": "^4.1.1", - "eslint-visitor-keys": "^1.1.0", "glob": "^7.1.6", "is-glob": "^4.0.1", "lodash": "^4.17.15", @@ -2614,23 +25009,14 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2638,22 +25024,25 @@ "dev": true }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, + "@typescript-eslint/visitor-keys": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -2890,7 +25279,8 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "6.2.0", @@ -2959,13 +25349,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true + "dev": true, + "requires": {} }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -3025,9 +25417,9 @@ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -3300,13 +25692,6 @@ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", "requires": { "follow-redirects": "^1.14.0" - }, - "dependencies": { - "follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" - } } }, "axios-mock-adapter": { @@ -3573,7 +25958,8 @@ "version": "0.3.7", "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.7.tgz", "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", - "dev": true + "dev": true, + "requires": {} }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", @@ -4383,9 +26769,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001178", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001178.tgz", - "integrity": "sha512-VtdZLC0vsXykKni8Uztx45xynytOi71Ufx9T8kHptSw9AL4dpqailUJJHavttuzUe1KYuBYtChiWv+BAb7mPmQ==", + "version": "1.0.30001323", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz", + "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==", "dev": true }, "capture-exit": { @@ -5022,6 +27408,22 @@ "requires": { "lru-cache": "^4.0.1", "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } } }, "crypto-browserify": { @@ -6539,7 +28941,8 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz", "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -7196,10 +29599,9 @@ } }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", - "dev": true + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "for-each": { "version": "0.3.3", @@ -7557,9 +29959,9 @@ } }, "globule": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", - "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.3.tgz", + "integrity": "sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg==", "requires": { "glob": "~7.1.1", "lodash": "~4.17.10", @@ -9267,9 +31669,9 @@ } }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", "dev": true, "requires": { "async-limiter": "~1.0.0" @@ -9699,7 +32101,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "24.9.0", @@ -10609,14 +33012,15 @@ "integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs=" }, "leaflet.locatecontrol": { - "version": "0.72.0", - "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.72.0.tgz", - "integrity": "sha512-enAf10UG9Z1bV0siTP/+vG/ZVncDqSA3V8c6iZ3s6KWL5Ngkk4A4mk9Ssefj46ey98I9HSYWqoS+k2Y7EaKjwQ==" + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/leaflet.locatecontrol/-/leaflet.locatecontrol-0.76.0.tgz", + "integrity": "sha512-Mx8uiihBi8KrrW3LgblsNL/pS8HR0gj60m8VFDFrnhSvDuitChazc095XcMSscf/XqZW+TSqQMCTe+AUy/4/eA==" }, "leaflet.markercluster": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz", - "integrity": "sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA==" + "integrity": "sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA==", + "requires": {} }, "left-pad": { "version": "1.3.0", @@ -10752,9 +33156,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.21", @@ -10837,12 +33241,12 @@ } }, "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "yallist": "^4.0.0" } }, "lz-string": { @@ -11278,9 +33682,9 @@ } }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" }, "move-concurrently": { "version": "1.0.1", @@ -13309,6 +35713,13 @@ "fast-diff": "^1.1.2" } }, + "prettier-plugin-organize-imports": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-2.3.4.tgz", + "integrity": "sha512-R8o23sf5iVL/U71h9SFUdhdOEPsi3nm42FD/oDYIZ2PQa4TNWWuWecxln6jlIQzpZTDMUeO1NicJP6lLn2TtRw==", + "dev": true, + "requires": {} + }, "pretty-bytes": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz", @@ -14116,6 +36527,87 @@ } } }, + "@typescript-eslint/eslint-plugin": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.34.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + } + } + }, + "@typescript-eslint/parser": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -14513,9 +37005,9 @@ } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "regexpu-core": { @@ -15760,9 +38252,9 @@ } }, "ssri": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.0.tgz", - "integrity": "sha512-77/WrDZUWocK0mvA5NTRQyveUf+wsrIc6vyrxpS8tVvYBcX215QbafrJR3KtkpskIzoFLqqNuuYQvxaMjXJ/0g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-7.1.1.tgz", + "integrity": "sha512-w+daCzXN89PseTL99MkA+fxJEcU3wfaE/ah0i0lnOlpG1CYLJ2ZjzEry68YBKfLs4JfoTShrTEsJkAZuNZ/stw==", "dev": true, "requires": { "figgy-pudding": "^3.5.1", @@ -15877,6 +38369,14 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -15949,14 +38449,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", @@ -16512,9 +39004,9 @@ } }, "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, "to-arraybuffer": { @@ -16629,9 +39121,9 @@ "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" }, "tsutils": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.19.1.tgz", - "integrity": "sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -16701,9 +39193,9 @@ "dev": true }, "typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==" + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz", + "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==" }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", @@ -16890,9 +39382,9 @@ } }, "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dev": true, "requires": { "querystringify": "^2.1.1", @@ -17248,9 +39740,9 @@ "dev": true }, "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", "dev": true, "requires": { "figgy-pudding": "^3.5.1" @@ -17464,9 +39956,9 @@ } }, "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", + "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", "dev": true, "requires": { "async-limiter": "~1.0.0" @@ -17596,11 +40088,11 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "requires": { - "string-width": "^1.0.2 || 2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, "wkt-parser": { @@ -17871,9 +40363,9 @@ } }, "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", + "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", "dev": true, "requires": { "async-limiter": "~1.0.0" @@ -17918,9 +40410,10 @@ "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "yaml": { "version": "1.10.0", diff --git a/app/package.json b/app/package.json index 2f65fce8f7..62060a2c0b 100644 --- a/app/package.json +++ b/app/package.json @@ -1,7 +1,7 @@ { - "name": "biohubbc-app", + "name": "sims-app", "version": "0.0.0", - "description": "BioHubBC - Web Application", + "description": "SIMS Web App", "license": "Apache-2.0", "repository": { "type": "git", @@ -21,7 +21,7 @@ "format:fix": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss}\"" }, "engines": { - "node": ">= 10.0.0", + "node": ">= 14.0.0", "npm": ">= 6.0.0" }, "dependencies": { @@ -32,15 +32,14 @@ "@material-ui/pickers": "~3.2.10", "@material-ui/styles": "~4.10.0", "@material-ui/system": "4.11.3", - "@mdi/js": "~5.9.55", + "@mdi/js": "~6.4.95", "@mdi/react": "~1.4.0", "@react-keycloak/web": "~2.1.0", "@react-leaflet/core": "~1.0.2", - "react-leaflet-cluster": "~1.0.3", "@tmcw/togeojson": "~4.2.0", "@turf/bbox": "~6.3.0", "@turf/boolean-equal": "~6.3.0", - "reproj-helper": "~1.2.8", + "@turf/center-of-mass": "~6.5.0", "@turf/centroid": "~6.4.0", "axios": "~0.21.4", "clsx": "~1.1.1", @@ -48,23 +47,25 @@ "formik": "~2.2.6", "keycloak-js": "~9.0.2", "leaflet": "~1.7.1", - "leaflet-draw": "~1.0.0", - "leaflet.locatecontrol": "~0.72.0", + "leaflet-draw": "~1.0.4", + "leaflet-fullscreen": "~1.0.2", + "leaflet.locatecontrol": "~0.76.0", "lodash-es": "~4.17.21", - "moment": "~2.29.1", + "moment": "~2.29.2", "node-sass": "~4.14.1", "qs": "~6.9.4", "react": "~16.14.0", "react-dom": "~16.14.0", "react-dropzone": "~11.3.2", "react-leaflet": "~3.1.0", - "leaflet-fullscreen": "~1.0.2", + "react-leaflet-cluster": "~1.0.3", "react-number-format": "~4.5.2", "react-router": "~5.1.2", "react-router-dom": "~5.1.2", "react-window": "~1.8.6", + "reproj-helper": "~1.2.8", "shpjs": "^3.6.3", - "typescript": "~3.9.4", + "typescript": "~4.1.6", "uuid": "~8.3.2", "yup": "~0.32.9" }, @@ -76,9 +77,10 @@ "@types/geojson": "~7946.0.7", "@types/jest": "~26.0.20", "@types/leaflet": "~1.5.23", - "@types/leaflet-fullscreen": "~1.0.5", + "@types/leaflet-fullscreen": "~1.0.6", "@types/lodash-es": "~4.17.4", - "@types/node": "~12.12.24", + "@types/node": "~14.14.31", + "@types/node-sass": "~4.11.2", "@types/qs": "~6.9.5", "@types/react": "~16.9.17", "@types/react-dom": "~16.9.4", @@ -88,6 +90,8 @@ "@types/react-window": "~1.8.2", "@types/shpjs": "^3.4.0", "@types/uuid": "~8.3.0", + "@typescript-eslint/eslint-plugin": "~3.10.1", + "@typescript-eslint/parser": "~3.10.1", "axios-mock-adapter": "~1.19.0", "eslint": "~6.8.0", "eslint-config-prettier": "~6.15.0", @@ -95,6 +99,7 @@ "jest": "~24.9.0", "jest-sonar-reporter": "~2.0.0", "prettier": "~2.2.1", + "prettier-plugin-organize-imports": "~2.3.4", "react-scripts": "~3.4.4" }, "browserslist": { @@ -128,7 +133,6 @@ "!/public/**", "!/build/**", "!/src/serviceWorker.**", - "!/src/setupProxy.*", "!/src/setupTests.*" ], "coverageThreshold": { diff --git a/app/server/index.js b/app/server/index.js index b9b2e415a8..7f54407772 100644 --- a/app/server/index.js +++ b/app/server/index.js @@ -51,9 +51,9 @@ const request = require('request'); clientId: process.env.SSO_CLIENT_ID || 'biohubbc' }, SITEMINDER_LOGOUT_URL: - process.env.REACT_APP_SITEMINDER_LOGOUT_URL || 'https://logontest.gov.bc.ca/clp-cgi/logoff.cgi', - MAX_UPLOAD_NUM_FILES: process.env.REACT_APP_MAX_UPLOAD_NUM_FILES, - MAX_UPLOAD_FILE_SIZE: process.env.REACT_APP_MAX_UPLOAD_FILE_SIZE + process.env.REACT_APP_SITEMINDER_LOGOUT_URL || 'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi', + MAX_UPLOAD_NUM_FILES: Number(process.env.REACT_APP_MAX_UPLOAD_NUM_FILES) || 10, + MAX_UPLOAD_FILE_SIZE: Number(process.env.REACT_APP_MAX_UPLOAD_FILE_SIZE) || 52428800 }; resp.status(200).json(config); }); diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index f0de9b26b3..2ec1a3536b 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -1,6 +1,15 @@ +import { + AuthenticatedRouteGuard, + SystemRoleRouteGuard, + UnAuthenticatedRouteGuard +} from 'components/security/RouteGuards'; import { SYSTEM_ROLE } from 'constants/roles'; import AdminUsersRouter from 'features/admin/AdminUsersRouter'; +import PermitsRouter from 'features/permits/PermitsRouter'; import ProjectsRouter from 'features/projects/ProjectsRouter'; +import PublicProjectsRouter from 'features/projects/PublicProjectsRouter'; +import ResourcesRouter from 'features/resources/ResourcesRouter'; +import SearchPage from 'features/search/SearchPage'; import PublicLayout from 'layouts/PublicLayout'; import RequestSubmitted from 'pages/200/RequestSubmitted'; import AccessDenied from 'pages/403/AccessDenied'; @@ -10,12 +19,8 @@ import LogOutPage from 'pages/logout/LogOutPage'; import React from 'react'; import { Redirect, Switch, useLocation } from 'react-router-dom'; import AppRoute from 'utils/AppRoute'; -import SearchPage from 'features/search/SearchPage'; -import PermitsRouter from 'features/permits/PermitsRouter'; -import ResourcesRouter from 'features/resources/ResourcesRouter'; -import PublicProjectsRouter from 'features/projects/PublicProjectsRouter'; -const AppRouter: React.FC = (props: any) => { +const AppRouter: React.FC = () => { const location = useLocation(); const getTitle = (page: string) => { @@ -24,81 +29,85 @@ const AppRouter: React.FC = (props: any) => { return ( - + + - - - - - - - - - - - - - - } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/app/src/components/attachments/AttachmentsList.test.tsx b/app/src/components/attachments/AttachmentsList.test.tsx index 5b96fdde3e..1b3929622e 100644 --- a/app/src/components/attachments/AttachmentsList.test.tsx +++ b/app/src/components/attachments/AttachmentsList.test.tsx @@ -1,7 +1,8 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; +import { AttachmentType } from '../../constants/attachments'; import AttachmentsList from './AttachmentsList'; -import { useBiohubApi } from 'hooks/useBioHubApi'; jest.mock('../../hooks/useBioHubApi'); const mockUseBiohubApi = { @@ -32,20 +33,29 @@ describe('AttachmentsList', () => { { id: 1, fileName: 'filename.test', + fileType: AttachmentType.OTHER, lastModified: '2021-04-09 11:53:53', - size: 3028 + size: 3028, + securityToken: null, + revisionCount: 1 }, { id: 20, fileName: 'filename20.test', + fileType: AttachmentType.REPORT, lastModified: '2021-04-09 11:53:53', - size: 30280000 + size: 30280000, + securityToken: null, + revisionCount: 1 }, { id: 30, fileName: 'filename30.test', + fileType: AttachmentType.OTHER, lastModified: '2021-04-09 11:53:53', - size: 30280000000 + size: 30280000000, + securityToken: null, + revisionCount: 1 } ]; diff --git a/app/src/components/attachments/AttachmentsList.tsx b/app/src/components/attachments/AttachmentsList.tsx index cac8e60103..f87112059c 100644 --- a/app/src/components/attachments/AttachmentsList.tsx +++ b/app/src/components/attachments/AttachmentsList.tsx @@ -2,7 +2,11 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import IconButton from '@material-ui/core/IconButton'; import Link from '@material-ui/core/Link'; -import Paper from '@material-ui/core/Paper'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import { Theme } from '@material-ui/core/styles/createMuiTheme'; +import makeStyles from '@material-ui/core/styles/makeStyles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; @@ -10,24 +14,39 @@ import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TablePagination from '@material-ui/core/TablePagination'; import TableRow from '@material-ui/core/TableRow'; -import makeStyles from '@material-ui/core/styles/makeStyles'; -import { Theme } from '@material-ui/core/styles/createMuiTheme'; -import { mdiLockOutline, mdiLockOpenVariantOutline, mdiTrashCanOutline } from '@mdi/js'; +import { + mdiDotsVertical, + mdiDownload, + mdiInformationOutline, + mdiLockOpenVariantOutline, + mdiLockOutline, + mdiTrashCanOutline +} from '@mdi/js'; import Icon from '@mdi/react'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; +import { AttachmentsI18N, EditReportMetaDataI18N } from 'constants/i18n'; import { DialogContext } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { IGetProjectAttachment } from 'interfaces/useProjectApi.interface'; +import { IGetProjectAttachment, IGetReportMetaData } from 'interfaces/useProjectApi.interface'; import { IGetSurveyAttachment } from 'interfaces/useSurveyApi.interface'; -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { handleChangePage, handleChangeRowsPerPage } from 'utils/tablePaginationUtils'; import { getFormattedDate, getFormattedFileSize } from 'utils/Utils'; +import { AttachmentType } from '../../constants/attachments'; +import { IEditReportMetaForm } from '../attachments/EditReportMetaForm'; +import EditFileWithMetaDialog from '../dialog/EditFileWithMetaDialog'; +import ViewFileWithMetaDialog from '../dialog/ViewFileWithMetaDialog'; const useStyles = makeStyles((theme: Theme) => ({ attachmentsTable: { '& .MuiTableCell-root': { verticalAlign: 'middle' } + }, + uploadMenu: { + marginTop: theme.spacing(1) } })); @@ -45,21 +64,63 @@ const AttachmentsList: React.FC = (props) => { const [rowsPerPage, setRowsPerPage] = useState(10); const [page, setPage] = useState(0); + const [reportMetaData, setReportMetaData] = useState(null); + const [showViewFileWithMetaDialog, setShowViewFileWithMetaDialog] = useState(false); + const [showEditFileWithMetaDialog, setShowEditFileWithMetaDialog] = useState(false); + + const [currentAttachment, setCurrentAttachment] = useState(null); + + const handleDownloadFileClick = (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { + openAttachment(attachment); + }; + + const handleDeleteFileClick = (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { + showDeleteAttachmentDialog(attachment); + }; + + const handleViewDetailsClick = (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { + setCurrentAttachment(attachment); + getReportMeta(attachment); + }; + const dialogContext = useContext(DialogContext); + const defaultErrorDialogProps = { + dialogTitle: EditReportMetaDataI18N.editErrorTitle, + dialogText: EditReportMetaDataI18N.editErrorText, + open: false, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }; + + const showErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ ...defaultErrorDialogProps, ...textDialogProps, open: true }); + }; + const defaultYesNoDialogProps = { - dialogTitle: 'Delete Attachment', - dialogText: 'Are you sure you want to delete the selected attachment?', + dialogTitle: 'Delete Document', + dialogText: 'Are you sure you want to delete the selected document? This action cannot be undone.', open: false, onClose: () => dialogContext.setYesNoDialog({ open: false }), onNo: () => dialogContext.setYesNoDialog({ open: false }), onYes: () => dialogContext.setYesNoDialog({ open: false }) }; + useEffect(() => { + if (reportMetaData && currentAttachment) { + setShowViewFileWithMetaDialog(true); + } + }, [reportMetaData, currentAttachment]); + const showDeleteAttachmentDialog = (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { dialogContext.setYesNoDialog({ ...defaultYesNoDialogProps, open: true, + yesButtonProps: { color: 'secondary' }, onYes: () => { deleteAttachment(attachment); dialogContext.setYesNoDialog({ open: false }); @@ -92,17 +153,15 @@ const AttachmentsList: React.FC = (props) => { } try { - let response; - if (!props.surveyId) { - response = await biohubApi.project.deleteProjectAttachment( + await biohubApi.project.deleteProjectAttachment( props.projectId, attachment.id, attachment.fileType, attachment.securityToken ); } else if (props.surveyId) { - response = await biohubApi.survey.deleteSurveyAttachment( + await biohubApi.survey.deleteSurveyAttachment( props.projectId, props.surveyId, attachment.id, @@ -111,24 +170,52 @@ const AttachmentsList: React.FC = (props) => { ); } + props.getAttachments(true); + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ + dialogTitle: AttachmentsI18N.deleteErrorTitle, + dialogText: AttachmentsI18N.deleteErrorText, + dialogErrorDetails: apiError.errors, + open: true + }); + return; + } + }; + + const getReportMeta = async (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { + try { + let response; + + if (props.surveyId) { + response = await biohubApi.survey.getSurveyReportMetadata(props.projectId, props.surveyId, attachment.id); + } else { + response = await biohubApi.project.getProjectReportMetadata(props.projectId, attachment.id); + } + if (!response) { return; } - props.getAttachments(true); + setReportMetaData(response); } catch (error) { return error; } }; - const viewFileContents = async (attachment: any) => { + const openAttachment = async (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { try { let response; if (props.surveyId) { - response = await biohubApi.survey.getSurveyAttachmentSignedURL(props.projectId, props.surveyId, attachment.id); + response = await biohubApi.survey.getSurveyAttachmentSignedURL( + props.projectId, + props.surveyId, + attachment.id, + attachment.fileType + ); } else { - response = await biohubApi.project.getAttachmentSignedURL(props.projectId, attachment.id); + response = await biohubApi.project.getAttachmentSignedURL(props.projectId, attachment.id, attachment.fileType); } if (!response) { @@ -137,10 +224,28 @@ const AttachmentsList: React.FC = (props) => { window.open(response); } catch (error) { - return error; + const apiError = error as APIError; + showErrorDialog({ + dialogTitle: AttachmentsI18N.downloadErrorTitle, + dialogText: AttachmentsI18N.downloadErrorText, + dialogErrorDetails: apiError.errors, + open: true + }); + return; + } + }; + + const openAttachmentFromReportMetaDialog = async () => { + if (currentAttachment) { + openAttachment(currentAttachment); } }; + const openEditReportMetaDialog = async () => { + setShowViewFileWithMetaDialog(false); + setShowEditFileWithMetaDialog(true); + }; + const makeAttachmentSecure = async (attachment: IGetProjectAttachment | IGetSurveyAttachment) => { if (!attachment || !attachment.id) { return; @@ -205,64 +310,118 @@ const AttachmentsList: React.FC = (props) => { } }; + const handleDialogEditSave = async (values: IEditReportMetaForm) => { + if (!reportMetaData) { + return; + } + + const fileMeta = values; + + try { + if (props.surveyId) { + await biohubApi.survey.updateSurveyReportMetadata( + props.projectId, + props.surveyId, + reportMetaData.attachment_id, + AttachmentType.REPORT, + fileMeta, + reportMetaData.revision_count + ); + } else { + await biohubApi.project.updateProjectReportMetadata( + props.projectId, + reportMetaData.attachment_id, + AttachmentType.REPORT, + fileMeta, + reportMetaData.revision_count + ); + } + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, dialogErrorDetails: apiError.errors, open: true }); + } finally { + setShowEditFileWithMetaDialog(false); + } + }; + return ( <> - + { + setShowViewFileWithMetaDialog(false); + }} + onDownload={openAttachmentFromReportMetaDialog} + reportMetaData={reportMetaData} + attachmentSize={(currentAttachment && getFormattedFileSize(currentAttachment.size)) || '0 KB'} + /> + { + setShowEditFileWithMetaDialog(false); + }} + onSave={handleDialogEditSave} + /> + Name Type - Last Modified File Size - Security Status + Last Modified + Security {props.attachmentsList.length > 0 && - props.attachmentsList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => ( - - - viewFileContents(row)}> - {row.fileName} - - - {row.fileType} - {getFormattedDate(DATE_FORMAT.ShortDateFormatMonthFirst, row.lastModified)} - {getFormattedFileSize(row.size)} - - - - - - - - showDeleteAttachmentDialog(row)}> - - - - - - ))} + props.attachmentsList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => { + return ( + + + openAttachment(row)}> + {row.fileName} + + + {row.fileType} + {getFormattedFileSize(row.size)} + {getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, row.lastModified)} + + + + + + + + + + + ); + })} {!props.attachmentsList.length && ( - + No Attachments @@ -283,9 +442,100 @@ const AttachmentsList: React.FC = (props) => { } /> )} - + ); }; export default AttachmentsList; + +interface IAttachmentItemMenuButtonProps { + attachment: IGetProjectAttachment | IGetSurveyAttachment; + handleDownloadFileClick: (attachment: IGetProjectAttachment | IGetSurveyAttachment) => void; + handleDeleteFileClick: (attachment: IGetProjectAttachment | IGetSurveyAttachment) => void; + handleViewDetailsClick: (attachment: IGetProjectAttachment | IGetSurveyAttachment) => void; +} + +const AttachmentItemMenuButton: React.FC = (props) => { + const classes = useStyles(); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (event: any) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + + { + props.handleDownloadFileClick(props.attachment); + setAnchorEl(null); + }} + data-testid="attachment-action-menu-download"> + + + + Download File + + {props.attachment.fileType === AttachmentType.REPORT && ( + { + props.handleViewDetailsClick(props.attachment); + setAnchorEl(null); + }} + data-testid="attachment-action-menu-details"> + + + + View Details + + )} + { + props.handleDeleteFileClick(props.attachment); + setAnchorEl(null); + }} + data-testid="attachment-action-menu-delete"> + + + + Delete File + + + + + + ); +}; diff --git a/app/src/components/attachments/DropZone.tsx b/app/src/components/attachments/DropZone.tsx index 9b6cb50ac8..f7096d1fbf 100644 --- a/app/src/components/attachments/DropZone.tsx +++ b/app/src/components/attachments/DropZone.tsx @@ -1,19 +1,25 @@ import Box from '@material-ui/core/Box'; import Link from '@material-ui/core/Link'; -import Typography from '@material-ui/core/Typography'; +import { Theme } from '@material-ui/core/styles/createMuiTheme'; import makeStyles from '@material-ui/core/styles/makeStyles'; -import { mdiUploadOutline } from '@mdi/js'; +import Typography from '@material-ui/core/Typography'; +import { mdiTrayArrowUp } from '@mdi/js'; import Icon from '@mdi/react'; +import { ConfigContext } from 'contexts/configContext'; import React, { useContext } from 'react'; import Dropzone, { FileRejection } from 'react-dropzone'; -import { ConfigContext } from 'contexts/configContext'; -const useStyles = makeStyles(() => ({ - textSpacing: { - marginBottom: '1rem' +const useStyles = makeStyles((theme: Theme) => ({ + dropZoneTitle: { + marginBottom: theme.spacing(1), + fontSize: '1.125rem', + fontWeight: 700 }, - browseLink: { - cursor: 'pointer' + dropZoneIcon: { + color: theme.palette.text.primary + '55' + }, + dropZoneRequirements: { + textAlign: 'center' } })); @@ -45,7 +51,24 @@ export interface IDropZoneConfigProps { * @memberof IDropZoneProps */ maxNumFiles?: number; - + /** + * Allow selecting multiple files while browsing. + * Default: true + * + * Note: Does not impact drag/drop. + * + * @type {boolean} + * @memberof IDropZoneProps + */ + multiple?: boolean; + /** + * Comma separated list of allowed file extensions. + * + * Example: `'.pdf, .txt'` + * + * @type {string} + * @memberof IDropZoneConfigProps + */ acceptedFileExtensions?: string; } @@ -55,39 +78,53 @@ export const DropZone: React.FC = (props) const maxNumFiles = props.maxNumFiles || config?.MAX_UPLOAD_NUM_FILES; const maxFileSize = props.maxFileSize || config?.MAX_UPLOAD_FILE_SIZE; + const multiple = props.multiple ?? true; const acceptedFileExtensions = props.acceptedFileExtensions; return ( - - {({ getRootProps, getInputProps }) => ( -
+ + + {({ getRootProps, getInputProps }) => ( - - - - Drag your files here, or Browse Files - - {acceptedFileExtensions && ( - - {`Accepted file types: ${acceptedFileExtensions}`} - - )} - {!!maxFileSize && maxFileSize !== Infinity && ( - - {`Maximum file size: ${Math.round(maxFileSize / BYTES_PER_MEGABYTE)} MB`} - - )} - {!!maxNumFiles && ( - - {`Maximum file count: ${maxNumFiles}`} - - )} + + + + Drag your {(multiple && 'files') || 'file'} here, or Browse Files + + + {acceptedFileExtensions && ( + + + {`Accepted files: ${acceptedFileExtensions}`} + + + )} + {!!maxFileSize && maxFileSize !== Infinity && ( + + + {`Maximum file size: ${Math.round(maxFileSize / BYTES_PER_MEGABYTE)} MB`} + + + )} + {!!maxNumFiles && ( + + + {`Maximum files: ${maxNumFiles}`} + + + )} + -
- )} -
+ )} + + ); }; diff --git a/app/src/components/attachments/EditFileWithMeta.tsx b/app/src/components/attachments/EditFileWithMeta.tsx new file mode 100644 index 0000000000..e4000f3a02 --- /dev/null +++ b/app/src/components/attachments/EditFileWithMeta.tsx @@ -0,0 +1,18 @@ +import Box from '@material-ui/core/Box'; +import { useFormikContext } from 'formik'; +import React from 'react'; +import EditReportMetaForm, { IEditReportMetaForm } from '../attachments/EditReportMetaForm'; + +export const EditFileWithMeta: React.FC = () => { + const { handleSubmit } = useFormikContext(); + + return ( +
+ + + + + ); +}; + +export default EditFileWithMeta; diff --git a/app/src/components/attachments/EditReportMetaForm.tsx b/app/src/components/attachments/EditReportMetaForm.tsx new file mode 100644 index 0000000000..3958f675db --- /dev/null +++ b/app/src/components/attachments/EditReportMetaForm.tsx @@ -0,0 +1,187 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import CustomTextField from 'components/fields/CustomTextField'; +import { FieldArray, useFormikContext } from 'formik'; +import React from 'react'; +import yup from 'utils/YupSchema'; + +export interface IEditReportMetaFormArrayItem { + first_name: string; + last_name: string; +} + +export const EditReportMetaFormArrayItemInitialValues: IEditReportMetaFormArrayItem = { + first_name: '', + last_name: '' +}; + +export interface IEditReportMetaForm { + title: string; + authors: IEditReportMetaFormArrayItem[]; + description: string; + year_published: number; + revision_count: number; +} + +export const EditReportMetaFormInitialValues: IEditReportMetaForm = { + title: '', + authors: [EditReportMetaFormArrayItemInitialValues], + description: '', + year_published: ('' as unknown) as number, + revision_count: 0 +}; + +export const EditReportMetaFormYupSchema = yup.object().shape({ + title: yup.string().max(300, 'Cannot exceed 300 characters').required('A report title is required'), + description: yup.string().max(3000, 'Cannot exceed 3000 characters').required('A report summary is required'), + year_published: yup + .number() + .min(1900, 'year must be between 1900 and 2199') + .max(2199, 'Year must be between 1900 and 2199') + .required('Year published required'), + authors: yup + .array() + .min(1, 'An author is required') + .of( + yup.object().shape({ + first_name: yup.string().max(300, 'Cannot exceed 300 characters').required('First name required'), + last_name: yup.string().max(300, 'Cannot exceed 300 characters').required('Last name required') + }) + ) + .isUniqueAuthor('Author names must be unique') +}); + +/** + * Upload Report Meta Data + * + * @return {*} + */ +const EditReportMetaForm: React.FC = () => { + const { values, getFieldMeta, errors } = useFormikContext(); + + return ( + <> + + + Report Details + + + + + + + + + + + + + + + + + Author(s) + + ( + + + {values.authors?.map((author, index) => { + const authorFirstNameMeta = getFieldMeta(`authors.[${index}].first_name`); + const authorLastNameMeta = getFieldMeta(`authors.[${index}].last_name`); + + return ( + + + + + + + + + + + { + + arrayHelpers.remove(index)}> + + + + } + + + ); + })} + + + {errors?.authors && !Array.isArray(errors?.authors) && ( + + {errors.authors} + + )} + + + + + + )} + /> + + + ); +}; + +export default EditReportMetaForm; diff --git a/app/src/components/attachments/FileUpload.tsx b/app/src/components/attachments/FileUpload.tsx index 57d208a53b..3255504993 100644 --- a/app/src/components/attachments/FileUpload.tsx +++ b/app/src/components/attachments/FileUpload.tsx @@ -5,12 +5,28 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import React, { useEffect, useState } from 'react'; import { FileError, FileRejection } from 'react-dropzone'; import DropZone, { IDropZoneConfigProps } from './DropZone'; -import { IUploadHandler, MemoizedFileUploadItem } from './FileUploadItem'; +import { + IFileHandler, + IOnUploadSuccess, + IUploadHandler, + MemoizedFileUploadItem, + UploadFileStatus +} from './FileUploadItem'; const useStyles = makeStyles((theme: Theme) => ({ dropZone: { - border: '2px dashed grey', - cursor: 'default' + clear: 'both', + borderRadius: '4px', + borderStyle: 'dashed', + borderWidth: '2px', + borderColor: theme.palette.text.disabled, + background: theme.palette.primary.main + '11', + transition: 'all ease-out 0.2s', + '&:hover, &:focus': { + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.primary.main + '22' + }, + cursor: 'pointer' } })); @@ -23,9 +39,66 @@ export interface IUploadFileListProps { files: IUploadFile[]; } +export type IReplaceHandler = () => void; + export interface IFileUploadProps { + /** + * Callback fired for each file in the list + * + * @type {IUploadHandler} + * @memberof IFileUploadProps + */ uploadHandler: IUploadHandler; - onSuccess?: (response: any) => void; // currently only supports single file uploads (multiple will overwrite each other) + /** + * Callback fired for each accepted file in the list (that do not have any `DropZone` errors (size, count, extension). + * + * @type {IFileHandler} + * @memberof IFileUploadProps + */ + fileHandler?: IFileHandler; + /** + * Callback fired when `uploadHandler` runs successfully fora given file. Will run once for each file that is + * uploaded. + * + * @type {IOnUploadSuccess} + * @memberof IFileUploadProps + */ + onSuccess?: IOnUploadSuccess; + /** + * Manually dictate the status. + * + * Note: some component events are automatically triggered based on a change of status. + * + * @type {UploadFileStatus} + * @memberof IFileUploadProps + */ + status?: UploadFileStatus; + /** + * If the component should replace the selected files, rather than appending them. + * Default: false + * + * Example: + * - WIth replace=false, selecting FileA and then selecting FileB will result in both FileA and FileB in the upload + * list. + * - With replace=true, selecting FileA and then selecting FileB will result in only FileB in the upload list. + * + * Note: This will not change how many files are uploaded, only how many files appear in the list. So if + * a file is in the middle of uploading when it is replaced, that file will still continue to upload even though it + * is not visible in the upload list. + * + * @type {boolean} + * @memberof IFileUploadProps + */ + replace?: boolean; + /** + * Callback fired when files are replaced. + * + * Note: Does nothing if `replace` is not set to `true`. + * + * @type {IReplaceHandler} + * @memberof IFileUploadProps + */ + onReplace?: IReplaceHandler; dropZoneProps?: Partial; } @@ -75,14 +148,24 @@ export const FileUpload: React.FC = (props) => { }); }); - setFiles((currentFiles) => [...currentFiles, ...newAcceptedFiles, ...newRejectedFiles]); - - setFileUploadItems( - fileUploadItems.concat([ + if (props.replace) { + // Replace current files with new files + setFiles([...newAcceptedFiles, ...newRejectedFiles]); + setFileUploadItems([ ...newAcceptedFiles.map((item) => getFileUploadItem(item.file, item.error)), ...newRejectedFiles.map((item) => getFileUploadItem(item.file, item.error)) - ]) - ); + ]); + props.onReplace?.(); + } else { + // Append new files to current files + setFiles((currentFiles) => [...currentFiles, ...newAcceptedFiles, ...newRejectedFiles]); + setFileUploadItems( + fileUploadItems.concat([ + ...newAcceptedFiles.map((item) => getFileUploadItem(item.file, item.error)), + ...newRejectedFiles.map((item) => getFileUploadItem(item.file, item.error)) + ]) + ); + } }; const getFileUploadItem = (file: File, error?: string) => { @@ -94,6 +177,8 @@ export const FileUpload: React.FC = (props) => { file={file} error={error} onCancel={() => setFileToRemove(file.name)} + fileHandler={props.fileHandler} + status={props.status} /> ); }; @@ -143,7 +228,7 @@ export const FileUpload: React.FC = (props) => { return ( - + diff --git a/app/src/components/attachments/FileUploadItem.tsx b/app/src/components/attachments/FileUploadItem.tsx index 41f74c97c4..6bd9aff357 100644 --- a/app/src/components/attachments/FileUploadItem.tsx +++ b/app/src/components/attachments/FileUploadItem.tsx @@ -5,7 +5,7 @@ import ListItem from '@material-ui/core/ListItem'; import { Theme } from '@material-ui/core/styles/createMuiTheme'; import makeStyles from '@material-ui/core/styles/makeStyles'; import Typography from '@material-ui/core/Typography'; -import { mdiCheck, mdiWindowClose } from '@mdi/js'; +import { mdiCheck, mdiFileOutline, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import axios, { CancelTokenSource } from 'axios'; import { APIError } from 'hooks/api/useAxios'; @@ -13,45 +13,41 @@ import useIsMounted from 'hooks/useIsMounted'; import React, { useCallback, useEffect, useState } from 'react'; const useStyles = makeStyles((theme: Theme) => ({ - uploadListItem: { - border: '1px solid grey' + uploadProgress: { + marginTop: theme.spacing(0.5) }, - completeIcon: { - color: theme.palette.success.main - }, - errorIcon: { - color: theme.palette.error.main - }, - linearProgressBar: { - height: '10px' + uploadListItemBox: { + width: '100%', + borderWidth: '1px', + borderStyle: 'solid', + borderColor: theme.palette.action.disabled, + borderRadius: '4px' }, uploadingColor: { - backgroundColor: 'rgba(25, 118, 210, 0.5)', // primary.main with reduced opacity - height: '5px' - }, - uploadingBarColor: { - backgroundColor: theme.palette.primary.main + color: theme.palette.primary.main }, completeColor: { - backgroundColor: 'rgba(76, 175, 80, 0.5)', // success.main with reduced opacity - height: '5px' + color: theme.palette.success.main }, - completeBarColor: { - backgroundColor: theme.palette.success.main + completeBgColor: { + background: theme.palette.success.main }, - failedColor: { - backgroundColor: 'rgba(244, 67, 54, 0.5)', // error.main with reduced opacity - height: '5px' + errorColor: { + color: theme.palette.error.main + }, + errorBgColor: { + background: theme.palette.error.main + '44' }, - failedBarColor: { - backgroundColor: theme.palette.error.main + fileIconColor: { + color: theme.palette.action.disabled } })); export enum UploadFileStatus { + STAGED = 'Ready for upload', PENDING = 'Pending', UPLOADING = 'Uploading', - FINISHING_UPLOAD = 'Finishing Upload', + FINISHING_UPLOAD = 'Finishing upload', FAILED = 'Failed', COMPLETE = 'Complete' } @@ -64,30 +60,36 @@ export interface IUploadFile { error?: string; } -export type IUploadHandler = ( +export type IUploadHandler = ( file: File, cancelToken: CancelTokenSource, handleFileUploadProgress: (progressEvent: ProgressEvent) => void -) => Promise; +) => Promise; + +export type IFileHandler = (file: File | null) => void; + +export type IOnUploadSuccess = (response: any) => void; export interface IFileUploadItemProps { uploadHandler: IUploadHandler; - onSuccess?: (response: any) => void; + onSuccess?: IOnUploadSuccess; file: File; error?: string; onCancel: () => void; + fileHandler?: IFileHandler; + status?: UploadFileStatus; } const FileUploadItem: React.FC = (props) => { const isMounted = useIsMounted(); const classes = useStyles(); - const { uploadHandler, onSuccess } = props; + const { uploadHandler, fileHandler, onSuccess } = props; const [file] = useState(props.file); const [error, setError] = useState(props.error); - const [status, setStatus] = useState(UploadFileStatus.PENDING); + const [status, setStatus] = useState(props.status || UploadFileStatus.PENDING); const [progress, setProgress] = useState(0); const [cancelToken] = useState(axios.CancelToken.source()); @@ -107,6 +109,8 @@ const FileUploadItem: React.FC = (props) => { return; } + fileHandler?.(file); + if (status !== UploadFileStatus.PENDING) { return; } @@ -146,7 +150,18 @@ const FileUploadItem: React.FC = (props) => { .catch(); setStatus(UploadFileStatus.UPLOADING); - }, [file, status, cancelToken, uploadHandler, onSuccess, isMounted, initiateCancel, error, handleFileUploadError]); + }, [ + file, + status, + cancelToken, + uploadHandler, + fileHandler, + onSuccess, + isMounted, + initiateCancel, + error, + handleFileUploadError + ]); useEffect(() => { if (!isMounted()) { @@ -179,23 +194,31 @@ const FileUploadItem: React.FC = (props) => { // trigger the parents onCancel hook, as this component is in a state where it can be safely cancelled props.onCancel(); + props.fileHandler?.(null); }, [initiateCancel, isSafeToCancel, props]); return ( - - - - - {file.name} - {error || status} - - + + + + + + + + + {file.name} + + + {error || status} + + + + setInitiateCancel(true)} /> + + - - setInitiateCancel(true)} /> - ); @@ -223,31 +246,39 @@ interface IActionButtonProps { const ActionButton: React.FC = (props) => { const classes = useStyles(); - if (props.status === UploadFileStatus.PENDING || props.status === UploadFileStatus.UPLOADING) { + if (props.status === UploadFileStatus.PENDING || props.status === UploadFileStatus.STAGED) { return ( - - props.onCancel()}> - - - + props.onCancel()}> + + + ); + } + + if (props.status === UploadFileStatus.UPLOADING) { + return ( + props.onCancel()}> + + ); } if (props.status === UploadFileStatus.COMPLETE) { return ( - - + + ); } if (props.status === UploadFileStatus.FAILED) { return ( - - props.onCancel()}> - - - + props.onCancel()} + className={classes.errorColor}> + + ); } @@ -275,11 +306,16 @@ interface IProgressBarProps { const ProgressBar: React.FC = (props) => { const classes = useStyles(); + if (props.status === UploadFileStatus.STAGED) { + return <>; + } + if (props.status === UploadFileStatus.FINISHING_UPLOAD) { return ( ); } @@ -289,7 +325,8 @@ const ProgressBar: React.FC = (props) => { ); } @@ -299,7 +336,8 @@ const ProgressBar: React.FC = (props) => { ); } @@ -309,7 +347,8 @@ const ProgressBar: React.FC = (props) => { ); }; diff --git a/app/src/components/attachments/FileUploadWithMeta.tsx b/app/src/components/attachments/FileUploadWithMeta.tsx new file mode 100644 index 0000000000..db8aa0e2da --- /dev/null +++ b/app/src/components/attachments/FileUploadWithMeta.tsx @@ -0,0 +1,67 @@ +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import { ProjectSurveyAttachmentValidExtensions } from 'constants/attachments'; +import { useFormikContext } from 'formik'; +import React from 'react'; +import { AttachmentType } from '../../constants/attachments'; +import ReportMetaForm, { IReportMetaForm } from '../attachments/ReportMetaForm'; +import FileUpload, { IReplaceHandler } from './FileUpload'; +import { IFileHandler, IOnUploadSuccess, IUploadHandler, UploadFileStatus } from './FileUploadItem'; + +export interface IFileUploadWithMetaProps { + attachmentType: AttachmentType.REPORT | AttachmentType.OTHER; + uploadHandler: IUploadHandler; + fileHandler?: IFileHandler; + onSuccess?: IOnUploadSuccess; +} + +export const FileUploadWithMeta: React.FC = (props) => { + const { handleSubmit, setFieldValue, errors } = useFormikContext(); + + const fileHandler: IFileHandler = (file) => { + setFieldValue('attachmentFile', file); + + props.fileHandler?.(file); + }; + + const replaceHandler: IReplaceHandler = () => { + setFieldValue('attachmentFile', null); + }; + + return ( +
+ {props.attachmentType === AttachmentType.REPORT && ( + + + + )} + {(props.attachmentType === AttachmentType.REPORT && ( + + + Attach File + + + {errors?.attachmentFile && ( + + {errors.attachmentFile} + + )} + + )) || } + + ); +}; + +export default FileUploadWithMeta; diff --git a/app/src/components/attachments/ReportMetaForm.tsx b/app/src/components/attachments/ReportMetaForm.tsx new file mode 100644 index 0000000000..36c83f412f --- /dev/null +++ b/app/src/components/attachments/ReportMetaForm.tsx @@ -0,0 +1,188 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import Typography from '@material-ui/core/Typography'; +import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import CustomTextField from 'components/fields/CustomTextField'; +import { FieldArray, useFormikContext } from 'formik'; +import React from 'react'; +import yup from 'utils/YupSchema'; + +export interface IReportMetaFormArrayItem { + first_name: string; + last_name: string; +} + +export const ReportMetaFormArrayItemInitialValues: IReportMetaFormArrayItem = { + first_name: '', + last_name: '' +}; + +export interface IReportMetaForm { + title: string; + authors: IReportMetaFormArrayItem[]; + description: string; + year_published: number; + attachmentFile: File; +} + +export const ReportMetaFormInitialValues: IReportMetaForm = { + title: '', + authors: [ReportMetaFormArrayItemInitialValues], + description: '', + year_published: ('' as unknown) as number, + attachmentFile: (undefined as unknown) as File +}; + +export const ReportMetaFormYupSchema = yup.object().shape({ + title: yup.string().max(300, 'Cannot exceed 300 characters').required('A report title is required'), + description: yup.string().max(3000, 'Cannot exceed 3000 characters').required('A report summary is required'), + year_published: yup + .number() + .min(1900, 'year must be between 1900 and 2199') + .max(2199, 'Year must be between 1900 and 2199') + .required('Year published required'), + attachmentFile: yup.mixed().required('A file is required'), + authors: yup + .array() + .min(1, 'An author is required') + .of( + yup.object().shape({ + first_name: yup.string().max(300, 'Cannot exceed 300 characters').required('First name required'), + last_name: yup.string().max(300, 'Cannot exceed 300 characters').required('Last name required') + }) + ) + .isUniqueAuthor('Author names must be unique') +}); + +/** + * Upload Report Meta Data + * + * @return {*} + */ +const ReportMetaForm: React.FC = () => { + const { values, getFieldMeta, errors } = useFormikContext(); + + return ( + <> + + + Report Details + + + + + + + + + + + + + + + + + Author(s) + + ( + + + {values.authors?.map((author, index) => { + const authorFirstNameMeta = getFieldMeta(`authors.[${index}].first_name`); + const authorLastNameMeta = getFieldMeta(`authors.[${index}].last_name`); + + return ( + + + + + + + + + + + { + + arrayHelpers.remove(index)}> + + + + } + + + ); + })} + + + {errors?.authors && !Array.isArray(errors?.authors) && ( + + {errors.authors} + + )} + + + + + + )} + /> + + + ); +}; + +export default ReportMetaForm; diff --git a/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap b/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap index e6c344b9a0..c3c8e165e9 100644 --- a/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap +++ b/app/src/components/attachments/__snapshots__/DropZone.test.tsx.snap @@ -2,9 +2,11 @@ exports[`DropZone matches the snapshot 1`] = ` -
+
-

Drag your files here, or Browse Files -

- +
- Accepted file types: .txt - - - Maximum file size: 50 MB - - - Maximum file count: 10 - +
+ + Accepted files: .txt + +
+
+ + Maximum file size: 50 MB + +
+
+ + Maximum files: 10 + +
+
-
+
`; diff --git a/app/src/components/boundary/FullScreenViewMapDialog.tsx b/app/src/components/boundary/FullScreenViewMapDialog.tsx new file mode 100644 index 0000000000..cb6c6a4a14 --- /dev/null +++ b/app/src/components/boundary/FullScreenViewMapDialog.tsx @@ -0,0 +1,65 @@ +import AppBar from '@material-ui/core/AppBar'; +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import { mdiArrowLeft } from '@mdi/js'; +import Icon from '@mdi/react'; +import React from 'react'; + +export interface IFullScreenViewMapProps { + open: boolean; + onClose: () => void; + map: any; + description: any; + layers: any; + mapTitle: string; + backButtonTitle: string; +} + +/** + * A dialog for displaying a component for editing purposes and giving the user the option to say + * `Yes`(Save) or `No`. + * + * @param {*} props + * @return {*} + */ +export const FullScreenViewMapDialog: React.FC = (props) => { + if (!props.open) { + return <>; + } + + return ( + + + + + + + + + + {props.mapTitle} + + + + Location description + + {props.description ? <>{props.description} : 'No Description'} + + {props.layers} + + {props.map} + + + ); +}; + +export default FullScreenViewMapDialog; diff --git a/app/src/components/boundary/InferredLocationDetails.tsx b/app/src/components/boundary/InferredLocationDetails.tsx new file mode 100644 index 0000000000..6be8c15280 --- /dev/null +++ b/app/src/components/boundary/InferredLocationDetails.tsx @@ -0,0 +1,47 @@ +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; +import React from 'react'; + +export interface IInferredLayers { + parks: string[]; + nrm: string[]; + env: string[]; + wmu: string[]; +} + +export interface IInferredLocationDetailsProps { + layers: IInferredLayers; +} + +const InferredLocationDetails: React.FC = (props) => { + const displayInferredLayersInfo = (data: any[], type: string) => { + if (!data.length) { + return; + } + + return ( + + + {type} ({data.length}) + + + {data.map((item: string, index: number) => ( + + {item} + {index < data.length - 1 && ', '} + + ))} + + ); + }; + + return ( + <> + {displayInferredLayersInfo(props.layers.nrm, 'Natural Resource Ministries Regions')} + {displayInferredLayersInfo(props.layers.env, 'Ministry of Environment Regions')} + {displayInferredLayersInfo(props.layers.parks, 'Parks and EcoReserves')} + + ); +}; + +export default InferredLocationDetails; diff --git a/app/src/components/boundary/MapBoundary.tsx b/app/src/components/boundary/MapBoundary.tsx index ed83e301b7..2fafdec665 100644 --- a/app/src/components/boundary/MapBoundary.tsx +++ b/app/src/components/boundary/MapBoundary.tsx @@ -1,83 +1,88 @@ import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import FormControl from '@material-ui/core/FormControl'; import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import makeStyles from '@material-ui/core/styles/makeStyles'; -import React, { useState, useEffect } from 'react'; -import Button from '@material-ui/core/Button'; -import ComponentDialog from 'components/dialog/ComponentDialog'; -import FileUpload from 'components/attachments/FileUpload'; -import { mdiUploadOutline } from '@mdi/js'; +import Alert from '@material-ui/lab/Alert'; +import { mdiRefresh, mdiTrayArrowUp } from '@mdi/js'; import Icon from '@mdi/react'; +import FileUpload from 'components/attachments/FileUpload'; +import { IUploadHandler } from 'components/attachments/FileUploadItem'; +import InferredLocationDetails, { IInferredLayers } from 'components/boundary/InferredLocationDetails'; +import ComponentDialog from 'components/dialog/ComponentDialog'; import MapContainer from 'components/map/MapContainer'; +import { ProjectSurveyAttachmentValidExtensions } from 'constants/attachments'; +import { FormikContextType } from 'formik'; import { Feature } from 'geojson'; +import get from 'lodash-es/get'; +import React, { useEffect, useState } from 'react'; import { calculateUpdatedMapBounds, handleGPXUpload, handleKMLUpload, handleShapefileUpload } from 'utils/mapBoundaryUploadHelpers'; -import FormControl from '@material-ui/core/FormControl'; -import InputLabel from '@material-ui/core/InputLabel'; -import Select from '@material-ui/core/Select'; -import MenuItem from '@material-ui/core/MenuItem'; -import { ProjectSurveyAttachmentValidExtensions } from 'constants/attachments'; -import { IUploadHandler } from 'components/attachments/FileUploadItem'; -const useStyles = makeStyles({ - bold: { - fontWeight: 'bold' - }, - uploadButton: { - border: '2px solid', - textTransform: 'capitalize', - fontWeight: 'bold' - } -}); +const useStyles = makeStyles(() => + createStyles({ + zoomToBoundaryExtentBtn: { + padding: '3px', + borderRadius: '4px', + background: '#ffffff', + color: '#000000', + border: '2px solid rgba(0,0,0,0.2)', + backgroundClip: 'padding-box', + '&:hover': { + backgroundColor: '#eeeeee' + } + }, + bold: { + fontWeight: 'bold' + }, + uploadButton: { + border: '2px solid', + textTransform: 'capitalize', + fontWeight: 'bold' + }, + mapLocations: { + '& dd': { + display: 'inline-block' + } + } + }) +); export interface IMapBoundaryProps { + name: string; title: string; mapId: string; - uploadError: string; - setUploadError: (error: string) => void; - values: any; bounds: any[]; - errors?: any; - setFieldValue: (key: string, value: any) => void; + formikProps: FormikContextType; } -export const displayInferredLayersInfo = (data: any[], type: string) => { - if (!data.length) { - return; - } - - return ( - - - {type} - - {data.map((item: string, index: number) => ( - - {item} - - ))} - - ); -}; - /** * Shared component for map boundary component * + * @param {*} props * @return {*} */ const MapBoundary: React.FC = (props) => { const classes = useStyles(); - const { title, mapId, uploadError, setUploadError, values, bounds, setFieldValue, errors } = props; + + const { name, title, mapId, bounds, formikProps } = props; + + const { values, errors, setFieldValue } = formikProps; const [openUploadBoundary, setOpenUploadBoundary] = useState(false); const [shouldUpdateBounds, setShouldUpdateBounds] = useState(false); const [updatedBounds, setUpdatedBounds] = useState(undefined); const [selectedLayer, setSelectedLayer] = useState(''); - const [inferredLayersInfo, setInferredLayersInfo] = useState({ + const [inferredLayersInfo, setInferredLayersInfo] = useState({ parks: [], nrm: [], env: [], @@ -89,13 +94,13 @@ const MapBoundary: React.FC = (props) => { }, [updatedBounds]); const boundaryUploadHandler = (): IUploadHandler => { - return (file, cancelToken, handleFileUploadProgress) => { + return (file) => { if (file?.type.includes('zip') || file?.name.includes('.zip')) { - handleShapefileUpload(file, values, setFieldValue, setUploadError); + handleShapefileUpload(file, name, formikProps); } else if (file?.type.includes('gpx') || file?.name.includes('.gpx')) { - handleGPXUpload(file, setUploadError, values, setFieldValue); + handleGPXUpload(file, name, formikProps); } else if (file?.type.includes('kml') || file?.name.includes('.kml')) { - handleKMLUpload(file, setUploadError, values, setFieldValue); + handleKMLUpload(file, name, formikProps); } return Promise.resolve(); @@ -109,7 +114,9 @@ const MapBoundary: React.FC = (props) => { dialogTitle="Upload Boundary" onClose={() => setOpenUploadBoundary(false)}> - Accepted file types: .gpx, .klm, .zip (shapefiles) + + If uploading a shapefile, it must be configured with a valid projection. + = (props) => { {title} - - You may select a boundary from an existing layer or upload a KML or Shapefile, KMZ files will not be - accepted. The Shapefile being uploaded must be configured with a valid projection. To select a boundary from - an existing layer, toggle the appropriate layer and select a boundary from the map, then press add boundary. - When done, press the hide layer button. + + Define your boundary by selecting a boundary from an existing layer or by uploading KML file or shapefile. + + + To select a boundary from an existing layer, select a layer from the dropdown, click a boundary on the map + and click 'Add Boundary'. + + @@ -174,38 +184,38 @@ const MapBoundary: React.FC = (props) => { )} - {uploadError && {uploadError}} - + + {get(errors, name) && {get(errors, name)}} + + setFieldValue('geometry', newGeo) + setGeometry: (newGeo: Feature[]) => setFieldValue(name, newGeo) }} bounds={(shouldUpdateBounds && updatedBounds) || bounds} selectedLayer={selectedLayer} setInferredLayersInfo={setInferredLayersInfo} /> + {values.geometry && values.geometry.length > 0 && ( + + { + setUpdatedBounds(calculateUpdatedMapBounds(values.geometry)); + setShouldUpdateBounds(true); + }}> + + + + )} - {errors && errors.geometry && ( + {get(errors, name) && ( - {errors.geometry} - - )} - {values.geometry && values.geometry.length > 0 && ( - - + {get(errors, name)} )} {!Object.values(inferredLayersInfo).every((item: any) => !item.length) && ( @@ -214,20 +224,7 @@ const MapBoundary: React.FC = (props) => { Boundary Information
- - - {displayInferredLayersInfo(inferredLayersInfo.nrm, 'NRM Regions')} - - - {displayInferredLayersInfo(inferredLayersInfo.env, 'ENV Regions')} - - - {displayInferredLayersInfo(inferredLayersInfo.wmu, 'WMU ID/GMZ ID/GMZ Name')} - - - {displayInferredLayersInfo(inferredLayersInfo.parks, 'Parks and EcoReserves')} - - +
)} diff --git a/app/src/components/chips/RequestChips.tsx b/app/src/components/chips/RequestChips.tsx new file mode 100644 index 0000000000..ab3e2349f0 --- /dev/null +++ b/app/src/components/chips/RequestChips.tsx @@ -0,0 +1,40 @@ +import Chip, { ChipProps } from '@material-ui/core/Chip'; +import { makeStyles, Theme } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import { AdministrativeActivityStatusType } from 'constants/misc'; +import React from 'react'; + +const useStyles = makeStyles((theme: Theme) => ({ + chip: { + color: 'white' + }, + chipPending: { + backgroundColor: theme.palette.primary.main + }, + chipActioned: { + backgroundColor: theme.palette.success.main + }, + chipRejected: { + backgroundColor: theme.palette.error.main + } +})); + +export const AccessStatusChip: React.FC<{ status: string; chipProps?: Partial }> = (props) => { + const classes = useStyles(); + + let chipLabel; + let chipStatusClass; + + if (props.status === AdministrativeActivityStatusType.REJECTED) { + chipLabel = 'Denied'; + chipStatusClass = classes.chipRejected; + } else if (props.status === AdministrativeActivityStatusType.ACTIONED) { + chipLabel = 'Approved'; + chipStatusClass = classes.chipActioned; + } else { + chipLabel = 'Pending'; + chipStatusClass = classes.chipPending; + } + + return ; +}; diff --git a/app/src/components/dialog/ComponentDialog.tsx b/app/src/components/dialog/ComponentDialog.tsx index 3bb0cb03ac..c945c78f3c 100644 --- a/app/src/components/dialog/ComponentDialog.tsx +++ b/app/src/components/dialog/ComponentDialog.tsx @@ -1,10 +1,10 @@ import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; +import Dialog, { DialogProps } from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; import useTheme from '@material-ui/core/styles/useTheme'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import React from 'react'; export interface IComponentDialogProps { @@ -28,6 +28,13 @@ export interface IComponentDialogProps { * @memberof IComponentDialogProps */ onClose: () => void; + /** + * `Dialog` props passthrough. + * + * @type {Partial} + * @memberof IComponentDialogProps + */ + dialogProps?: Partial; } /** @@ -53,7 +60,8 @@ const ComponentDialog: React.FC = (props) => { maxWidth="xl" open={props.open} aria-labelledby="component-dialog-title" - aria-describedby="component-dialog-description"> + aria-describedby="component-dialog-description" + {...props.dialogProps}> {props.dialogTitle} {props.children} diff --git a/app/src/components/dialog/EditDialog.tsx b/app/src/components/dialog/EditDialog.tsx index 00b73e7bdf..7e5a1dc06b 100644 --- a/app/src/components/dialog/EditDialog.tsx +++ b/app/src/components/dialog/EditDialog.tsx @@ -5,16 +5,16 @@ import DialogContent from '@material-ui/core/DialogContent'; import DialogTitle from '@material-ui/core/DialogTitle'; import useTheme from '@material-ui/core/styles/useTheme'; import useMediaQuery from '@material-ui/core/useMediaQuery'; -import { Formik } from 'formik'; -import React from 'react'; +import { Formik, FormikValues } from 'formik'; +import React, { PropsWithChildren } from 'react'; -export interface IEditDialogComponentProps { +export interface IEditDialogComponentProps { element: any; - initialValues: any; + initialValues: T; validationSchema: any; } -export interface IEditDialogProps { +export interface IEditDialogProps { /** * The dialog window title text. * @@ -44,7 +44,7 @@ export interface IEditDialogProps { * @type {IEditDialogComponentProps} * @memberof IEditDialogProps */ - component: IEditDialogComponentProps; + component: IEditDialogComponentProps; /** * Error message to display when an error exists @@ -62,17 +62,18 @@ export interface IEditDialogProps { * * @memberof IEditDialogProps */ - onSave: (values: any) => void; + onSave: (values: T) => void; } /** * A dialog for displaying a component for editing purposes and giving the user the option to say * `Yes`(Save) or `No`. * - * @param {*} props + * @template T + * @param {PropsWithChildren>} props * @return {*} */ -export const EditDialog: React.FC = (props) => { +export const EditDialog = (props: PropsWithChildren>) => { const theme = useTheme(); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); diff --git a/app/src/components/dialog/EditFileWithMetaDialog.tsx b/app/src/components/dialog/EditFileWithMetaDialog.tsx new file mode 100644 index 0000000000..be7a2b8ae2 --- /dev/null +++ b/app/src/components/dialog/EditFileWithMetaDialog.tsx @@ -0,0 +1,144 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import useTheme from '@material-ui/core/styles/useTheme'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import EditFileWithMeta from 'components/attachments/EditFileWithMeta'; +import { Formik, FormikProps } from 'formik'; +import { IGetReportMetaData } from 'interfaces/useProjectApi.interface'; +import React, { useRef, useState } from 'react'; +import { + EditReportMetaFormInitialValues, + EditReportMetaFormYupSchema, + IEditReportMetaForm +} from '../attachments/EditReportMetaForm'; + +const useStyles = makeStyles((theme) => ({ + wrapper: { + position: 'relative' + }, + buttonProgress: { + color: theme.palette.primary.main, + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12 + } +})); + +/** + * + * + * @export + * @interface IEditFileWithMetaDialogProps + */ +export interface IEditFileWithMetaDialogProps { + /** + * The dialog window title text. + * + * @type {string} + * @memberof IEditFileWithMetaDialogProps + */ + dialogTitle: string; + /** + * Report meta data + * + * @type {IGetReportMetaData | null} + * @memberof IEditFileWithMetaDialogProps + */ + reportMetaData: IGetReportMetaData | null; + /** + * Set to `true` to open the dialog, `false` to close the dialog. + * + * @type {boolean} + * @memberof IEditFileWithMetaDialogProps + */ + open: boolean; + /** + * Callback fired if the dialog is closed. + * + * @memberof IEditFileWithMetaDialogProps + */ + onClose: () => void; + /** + * Callback fired if the dialog save is clicked. + * + * @memberof IEditFileWithMetaDialogProps + */ + onSave: (fileMeta: IEditReportMetaForm) => Promise; +} + +/** + * A dialog to wrap any component(s) that need to be displayed as a modal. + * + * Any component(s) passed in `props.children` will be rendered as the content of the dialog. + * + * @param {*} props + * @return {*} + */ +const EditFileWithMetaDialog: React.FC = (props) => { + const theme = useTheme(); + + const classes = useStyles(); + + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const [formikRef] = useState(useRef>(null)); + + const [isSaving, setIsSaving] = useState(false); + + if (!props.open) { + return <>; + } + + return ( + + { + setIsSaving(true); + props.onSave(values).finally(() => { + setIsSaving(false); + props.onClose(); + }); + }}> + {(formikProps) => ( + <> + {props.dialogTitle} + + + + + + + {isSaving && } + + + + + )} + + + ); +}; + +export default EditFileWithMetaDialog; diff --git a/app/src/components/dialog/FileUploadWithMetaDialog.tsx b/app/src/components/dialog/FileUploadWithMetaDialog.tsx new file mode 100644 index 0000000000..8df7ae535e --- /dev/null +++ b/app/src/components/dialog/FileUploadWithMetaDialog.tsx @@ -0,0 +1,162 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import useTheme from '@material-ui/core/styles/useTheme'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import FileUploadWithMeta from 'components/attachments/FileUploadWithMeta'; +import { Formik, FormikProps } from 'formik'; +import React, { useRef, useState } from 'react'; +import { AttachmentType } from '../../constants/attachments'; +import { IFileHandler, IUploadHandler } from '../attachments/FileUploadItem'; +import { IReportMetaForm, ReportMetaFormInitialValues, ReportMetaFormYupSchema } from '../attachments/ReportMetaForm'; + +const useStyles = makeStyles((theme) => ({ + wrapper: { + position: 'relative' + }, + buttonProgress: { + color: theme.palette.primary.main, + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12 + } +})); + +/** + * + * + * @export + * @interface IFileUploadWithMetaDialogProps + */ +export interface IFileUploadWithMetaDialogProps { + /** + * The dialog window title text. + * + * @type {string} + * @memberof IFileUploadWithMetaDialogProps + */ + dialogTitle: string; + /** + * The type of attachment. + * + * @type {('Report' | 'Other')} + * @memberof IFileUploadWithMetaDialogProps + */ + attachmentType: AttachmentType.REPORT | AttachmentType.OTHER; + /** + * Set to `true` to open the dialog, `false` to close the dialog. + * + * @type {boolean} + * @memberof IFileUploadWithMetaDialogProps + */ + open: boolean; + /** + * Callback fired if the dialog is finished. + * + * @memberof IFileUploadWithMetaDialogProps + */ + onFinish: (fileMeta: IReportMetaForm) => Promise; + /** + * Callback fired if the dialog is closed. + * + * @memberof IFileUploadWithMetaDialogProps + */ + onClose: () => void; + /** + * Callback fired if an upload request is initiated. + * + * @memberof IFileUploadWithMetaDialogProps + */ + uploadHandler: IUploadHandler; + /** + * Callback fired if a file is added (via browser or drag/drop). + * + * @type {IFileHandler} + * @memberof IFileUploadWithMetaDialogProps + */ + fileHandler?: IFileHandler; +} + +/** + * A dialog to wrap any component(s) that need to be displayed as a modal. + * + * Any component(s) passed in `props.children` will be rendered as the content of the dialog. + * + * @param {*} props + * @return {*} + */ +const FileUploadWithMetaDialog: React.FC = (props) => { + const theme = useTheme(); + + const classes = useStyles(); + + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + + const [formikRef] = useState(useRef>(null)); + + const [isFinishing, setIsFinishing] = useState(false); + + if (!props.open) { + return <>; + } + + return ( + + { + setIsFinishing(true); + props.onFinish(values).finally(() => { + setIsFinishing(false); + props.onClose(); + }); + }}> + {(formikProps) => ( + <> + {props.dialogTitle} + + + + + {props.attachmentType === AttachmentType.REPORT && ( + + + {isFinishing && } + + )} + {(props.attachmentType === AttachmentType.REPORT && ( + + )) || ( + + )} + + + )} + + + ); +}; + +export default FileUploadWithMetaDialog; diff --git a/app/src/components/dialog/RequestDialog.tsx b/app/src/components/dialog/RequestDialog.tsx index 9358691223..5524462846 100644 --- a/app/src/components/dialog/RequestDialog.tsx +++ b/app/src/components/dialog/RequestDialog.tsx @@ -81,6 +81,7 @@ const RequestDialog: React.FC = (props) => { + {showEditButton && ( + + )} + + + + + ); +}; + +export default ViewFileWithMetaDialog; diff --git a/app/src/components/dialog/YesNoDialog.test.tsx b/app/src/components/dialog/YesNoDialog.test.tsx index 4900c4bfae..0776f384de 100644 --- a/app/src/components/dialog/YesNoDialog.test.tsx +++ b/app/src/components/dialog/YesNoDialog.test.tsx @@ -61,7 +61,10 @@ describe('EditDialog', () => { }); it('calls the onNo prop when `No` button is clicked', async () => { - const { findByText } = renderContainer({ dialogTitle: 'this is a test', dialogText: 'this is text' }); + const { findByText } = renderContainer({ + dialogTitle: 'this is a test', + dialogText: 'this is text' + }); const NoButton = await findByText('No', { exact: false }); diff --git a/app/src/components/dialog/YesNoDialog.tsx b/app/src/components/dialog/YesNoDialog.tsx index 5266635fa5..b899d87d84 100644 --- a/app/src/components/dialog/YesNoDialog.tsx +++ b/app/src/components/dialog/YesNoDialog.tsx @@ -1,12 +1,19 @@ -import Button from '@material-ui/core/Button'; +import Button, { ButtonProps } from '@material-ui/core/Button'; import Dialog from '@material-ui/core/Dialog'; import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; -import React from 'react'; +import React, { ReactNode } from 'react'; export interface IYesNoDialogProps { + /** + * optional component to render underneath the dialog text. + * + * @type {ReactNode} + * @memberof IYesNoDialogProps + */ + dialogContent?: ReactNode; /** * The dialog window title text. * @@ -46,6 +53,38 @@ export interface IYesNoDialogProps { * @memberof IYesNoDialogProps */ onYes: () => void; + + /** + * The yes button label. + * + * @type {string} + * @memberof IYesNoDialogProps + */ + yesButtonLabel?: string; + + /** + * The no button label. + * + * @type {string} + * @memberof IYesNoDialogProps + */ + noButtonLabel?: string; + + /** + * Optional yes-button props + * + * @type {Partial} + * @memberof IYesNoDialogProps + */ + yesButtonProps?: Partial; + + /** + * Optional no-button props + * + * @type {Partial} + * @memberof IYesNoDialogProps + */ + noButtonProps?: Partial; } /** @@ -64,18 +103,31 @@ const YesNoDialog: React.FC = (props) => { {props.dialogTitle} - {props.dialogText} + {props.dialogText && {props.dialogText}} + {props.dialogContent} - - diff --git a/app/src/components/dialog/__snapshots__/EditDialog.test.tsx.snap b/app/src/components/dialog/__snapshots__/EditDialog.test.tsx.snap index d2e054d2a0..8dc18dac12 100644 --- a/app/src/components/dialog/__snapshots__/EditDialog.test.tsx.snap +++ b/app/src/components/dialog/__snapshots__/EditDialog.test.tsx.snap @@ -53,6 +53,7 @@ exports[`EditDialog matches snapshot when open, with error message 1`] = `
@@ -78,8 +75,8 @@ const SurveysList: React.FC = (props) => { Name Species Timeline - Completion Status - Publish Status + Status + Published @@ -91,21 +88,27 @@ const SurveysList: React.FC = (props) => { underline="always" component="button" variant="body2" - onClick={() => history.push(`/admin/projects/${props.projectId}/surveys/${row.id}/details`)}> - {row.name} + onClick={() => + history.push(`/admin/projects/${props.projectId}/surveys/${row.survey.id}/details`) + }> + {row.survey.name} - {row.species?.join(', ')} + {row.species?.species_names?.join(', ')} - {getFormattedDateRangeString(DATE_FORMAT.ShortMediumDateFormat2, row.start_date, row.end_date)} + {getFormattedDateRangeString( + DATE_FORMAT.ShortMediumDateFormat, + row.survey.start_date, + row.survey.end_date + )} - {getChipIcon(row.completion_status)} - {getChipIcon(row.publish_status)} + {getChipIcon(row.survey.completion_status)} + {getChipIcon(row.survey.publish_status)} ))} {!props.surveysList.length && ( - + No Surveys @@ -126,7 +129,7 @@ const SurveysList: React.FC = (props) => { } /> )} - + ); }; diff --git a/app/src/components/toolbar/ActionToolbars.tsx b/app/src/components/toolbar/ActionToolbars.tsx new file mode 100644 index 0000000000..910f62b375 --- /dev/null +++ b/app/src/components/toolbar/ActionToolbars.tsx @@ -0,0 +1,252 @@ +import Box from '@material-ui/core/Box'; +import Button, { ButtonProps } from '@material-ui/core/Button'; +import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import Toolbar, { ToolbarProps } from '@material-ui/core/Toolbar'; +import Typography, { TypographyProps } from '@material-ui/core/Typography'; +import React, { ReactNode, useState } from 'react'; + +export interface ICustomButtonProps { + buttonLabel: string; + buttonTitle: string; + buttonOnClick: () => void; + buttonStartIcon: ReactNode; + buttonEndIcon?: ReactNode; + buttonProps?: Partial & { 'data-testid'?: string }; +} + +export interface IButtonToolbarProps extends ICustomButtonProps, IActionToolbarProps {} + +export const H3ButtonToolbar: React.FC = (props) => { + const id = `h3-button-toolbar-${props.buttonLabel.replace(/\s/g, '')}`; + + return ( + + + + ); +}; + +export const H2ButtonToolbar: React.FC = (props) => { + const id = `h2-button-toolbar-${props.buttonLabel.replace(/\s/g, '')}`; + + return ( + + + + ); +}; + +export interface IMenuToolbarItem { + menuIcon?: ReactNode; + menuLabel: string; + menuOnClick: () => void; +} + +export interface IMenuToolbarProps extends ICustomMenuButtonProps, IActionToolbarProps {} + +export const H2MenuToolbar: React.FC = (props) => { + return ( + + + + ); +}; + +export interface ICustomMenuButtonProps { + buttonLabel?: string; + buttonTitle: string; + buttonStartIcon?: ReactNode; + buttonEndIcon?: ReactNode; + buttonProps?: Partial & { 'data-testid'?: string }; + menuItems: IMenuToolbarItem[]; +} + +export const CustomMenuButton: React.FC = (props) => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const buttonId = `custom-menu-button-${props.buttonLabel?.replace(/\s/g, '') || 'button'}`; + + const handleClick = (event: any) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const closeMenuOnItemClick = (menuItemOnClick: () => void) => { + setAnchorEl(null); + menuItemOnClick(); + }; + + return ( + <> + + + {props.menuItems.map((menuItem) => { + const menuItemId = `custom-menu-button-item-${menuItem.menuLabel.replace(/\s/g, '')}`; + return ( + closeMenuOnItemClick(menuItem.menuOnClick)}> + {menuItem.menuIcon && {menuItem.menuIcon}} + {menuItem.menuLabel} + + ); + })} + + + ); +}; + +export interface ICustomMenuIconButtonProps { + buttonTitle: string; + buttonIcon: ReactNode; + buttonProps?: Partial; + menuItems: IMenuToolbarItem[]; +} + +export const CustomMenuIconButton: React.FC = (props) => { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + + const buttonId = `custom-menu-icon-${props.buttonTitle?.replace(/\s/g, '') || 'button'}`; + + const handleClick = (event: any) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const closeMenuOnItemClick = (menuItemOnClick: () => void) => { + setAnchorEl(null); + menuItemOnClick(); + }; + + return ( + <> + + {props.buttonIcon} + + + {props.menuItems.map((menuItem) => { + const menuItemId = `custom-menu-icon-item-${menuItem.menuLabel.replace(/\s/g, '')}`; + return ( + closeMenuOnItemClick(menuItem.menuOnClick)}> + {menuItem.menuIcon && {menuItem.menuIcon}} + {menuItem.menuLabel} + + ); + })} + + + ); +}; + +interface IActionToolbarProps { + label: string; + labelProps?: Partial>; + toolbarProps?: Partial; +} + +const ActionToolbar: React.FC = (props) => { + return ( + + + {props.label} + + {props.children} + + ); +}; diff --git a/app/src/constants/attachments.ts b/app/src/constants/attachments.ts index 3903e98b73..06dbef7a12 100644 --- a/app/src/constants/attachments.ts +++ b/app/src/constants/attachments.ts @@ -1,10 +1,6 @@ -export enum ProjectSurveyAttachmentType { - AUDIO = 'Audio', - DATA = 'Data File', - IMAGE = 'Image', +export enum AttachmentType { REPORT = 'Report', - SPATIAL = 'Spatial File', - VIDEO = 'Video' + OTHER = 'Other' } export enum ProjectSurveyAttachmentValidExtensions { diff --git a/app/src/constants/i18n.ts b/app/src/constants/i18n.ts index 09b4a0d964..0f510e8e8d 100644 --- a/app/src/constants/i18n.ts +++ b/app/src/constants/i18n.ts @@ -22,12 +22,18 @@ export const CreatePermitsI18N = { 'An error has occurred while attempting to create your permits, please try again. If the error persists, please contact your system administrator.' }; -export const UploadProjectAttachmentsI18N = { +export const AttachmentsI18N = { cancelTitle: 'Cancel Upload', cancelText: 'Are you sure you want to cancel?', - uploadErrorTitle: 'Error Uploading Project Attachments', + uploadErrorTitle: 'Error Uploading Attachments', uploadErrorText: - 'An error has occurred while attempting to upload project attachments, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to upload attachments, please try again. If the error persists, please contact your system administrator.', + deleteErrorTitle: 'Error Deleting Attachment', + deleteErrorText: + 'An error has occurred while attempting to delete attachments, please try again. If the error persists, please contact your system administrator.', + downloadErrorTitle: 'Error Downloading Attachment', + downloadErrorText: + 'An error has occurred while attempting to download an attachment, please try again. If the error persists, please contact your system administrator.' }; export const CreateProjectDraftI18N = { @@ -51,10 +57,10 @@ export const EditObjectivesI18N = { }; export const EditCoordinatorI18N = { - editTitle: 'Edit Project Coordinator', - editErrorTitle: 'Error Editing Project Coordinator', + editTitle: 'Edit Project Contact', + editErrorTitle: 'Error Editing Project Contact', editErrorText: - 'An error has occurred while attempting to edit your project coordinator details, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to edit your project contact details, please try again. If the error persists, please contact your system administrator.' }; export const EditGeneralInformationI18N = { @@ -85,11 +91,18 @@ export const EditSurveyProprietorI18N = { 'An error has occurred while attempting to edit your survey proprietor information, please try again. If the error persists, please contact your system administrator.' }; +export const EditSurveyPurposeAndMethodologyI18N = { + editTitle: 'Edit Survey Purpose and Methodology', + editErrorTitle: 'Error Editing Survey Purpose and Methodology', + editErrorText: + 'An error has occurred while attempting to edit your survey purpose and methodology information, please try again. If the error persists, please contact your system administrator.' +}; + export const EditLocationBoundaryI18N = { - editTitle: 'Edit Location / Project Boundary', - editErrorTitle: 'Error Editing Location / Project Boundary', + editTitle: 'Edit Project Location', + editErrorTitle: 'Error Editing Project Location', editErrorText: - 'An error has occurred while attempting to edit your location boundary, please try again. If the error persists, please contact your system administrator.' + 'An error has occurred while attempting to edit your location, please try again. If the error persists, please contact your system administrator.' }; export const EditIUCNI18N = { @@ -171,3 +184,62 @@ export const PublishProjectI18N = { publishErrorText: 'An error has occurred while attempting to publish this project, please try again. If the error persists, please contact your system administrator.' }; + +export const DeleteSurveyI18N = { + deleteTitle: 'Delete Survey', + deleteText: 'Are you sure you want to delete this survey, its attachments and associated observations?', + deleteErrorTitle: 'Error Deleting Project', + deleteErrorText: + 'An error has occurred while attempting to delete this project, its attachments and associated surveys/observations, please try again. If the error persists, please contact your system administrator.' +}; + +export const PublishSurveyI18N = { + publishTitle: 'Publish Survey', + publishText: 'Are you sure you want to publish this survey?', + publishErrorTitle: 'Error Publishing Survey', + publishErrorText: + 'An error has occurred while attempting to publish this survey, please try again. If the error persists, please contact your system administrator.' +}; + +export const EditReportMetaDataI18N = { + editTitle: 'Edit Report Meta Data', + editErrorTitle: 'Error Editing Report Meta Data', + editErrorText: + 'An error has occurred while attempting to edit your report meta data, please try again. If the error persists, please contact your system administrator.' +}; + +export const DeleteSystemUserI18N = { + deleteErrorTitle: 'Error Deleting System User', + deleteErrorText: + 'An error has occurred while attempting to delete the system user, please try again. If the error persists, please contact your system administrator.' +}; + +export const ProjectParticipantsI18N = { + getParticipantsErrorTitle: 'Error Fetching Project Team Members', + getParticipantsErrorText: + 'An error has occurred while attempting to fetch project team members, please try again. If the error persists, please contact your system administrator.', + addParticipantsErrorTitle: 'Error Adding Project Team Members', + addParticipantsErrorText: + 'An error has occurred while attempting to add project team members, please try again. If the error persists, please contact your system administrator.', + removeParticipantTitle: 'Remove Team Member?', + removeParticipantErrorTitle: 'Error Removing Project Team Member', + removeParticipantErrorText: + 'An error has occurred while attempting to remove the project team member, please try again. If the error persists, please contact your system administrator.', + updateParticipantRoleErrorTitle: 'Error Updating Project Role', + updateParticipantRoleErrorText: + "An error has occurred while attempting to update the user's project role, please try again. If the error persists, please contact your system administrator." +}; + +export const SystemUserI18N = { + deleteProjectLeadErrorTitle: 'Error Deleting Project Lead', + deleteProjectLeadErrorText: + 'An error has occurred while attempting to delete the project lead, please assign a different project lead before removing. Please try again, if the error persists please contact your system administrator.', + updateProjectLeadRoleErrorTitle: 'Error Updating Project Lead Role', + updateProjectLeadRoleErrorText: + "An error has occurred while attempting to update the user's project lead role, please assign a different project lead before changing. Please try again, if the error persists please contact your system administrator.", + removeSystemUserTitle: 'Remove System User ', + removeUserFromProject: 'Remove User From Project', + removeUserErrorTitle: 'Error Removing User From Team', + removeUserErrorText: + 'An error has occurred while attempting to remove the user from the team, please try again. If the error persists, please contact your system administrator.' +}; diff --git a/app/src/constants/roles.ts b/app/src/constants/roles.ts index c68fd335e1..eaee8993df 100644 --- a/app/src/constants/roles.ts +++ b/app/src/constants/roles.ts @@ -1,10 +1,23 @@ /** - * System level roles + * System level roles. * * @export * @enum {number} */ export enum SYSTEM_ROLE { SYSTEM_ADMIN = 'System Administrator', - PROJECT_ADMIN = 'Project Administrator' + PROJECT_CREATOR = 'Creator', + DATA_ADMINISTRATOR = 'Data Administrator' +} + +/** + * Project level roles. + * + * @export + * @enum {number} + */ +export enum PROJECT_ROLE { + PROJECT_LEAD = 'Project Lead', + PROJECT_EDITOR = 'Editor', + PROJECT_VIEWER = 'Viewer' } diff --git a/app/src/contexts/configContext.tsx b/app/src/contexts/configContext.tsx index 74d81848c3..874ee56ded 100644 --- a/app/src/contexts/configContext.tsx +++ b/app/src/contexts/configContext.tsx @@ -60,9 +60,9 @@ const getLocalConfig = (): IConfig => { clientId: process.env.SSO_CLIENT_ID || 'biohubbc' }, SITEMINDER_LOGOUT_URL: - process.env.REACT_APP_SITEMINDER_LOGOUT_URL || 'https://logontest.gov.bc.ca/clp-cgi/logoff.cgi', - MAX_UPLOAD_NUM_FILES: Number(process.env.MAX_UPLOAD_NUM_FILES) || 10, - MAX_UPLOAD_FILE_SIZE: Number(process.env.MAX_UPLOAD_FILE_SIZE) || 52428800 + process.env.REACT_APP_SITEMINDER_LOGOUT_URL || 'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi', + MAX_UPLOAD_NUM_FILES: Number(process.env.REACT_APP_MAX_UPLOAD_NUM_FILES) || 10, + MAX_UPLOAD_FILE_SIZE: Number(process.env.REACT_APP_MAX_UPLOAD_FILE_SIZE) || 52428800 }; }; diff --git a/app/src/contexts/dialogContext.tsx b/app/src/contexts/dialogContext.tsx index 2c732f6b92..fe271a9abc 100644 --- a/app/src/contexts/dialogContext.tsx +++ b/app/src/contexts/dialogContext.tsx @@ -1,8 +1,10 @@ +import IconButton from '@material-ui/core/IconButton'; import Snackbar from '@material-ui/core/Snackbar'; -import Alert, { Color } from '@material-ui/lab/Alert'; +import CloseIcon from '@material-ui/icons/Close'; +import { Color } from '@material-ui/lab/Alert'; import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; import YesNoDialog, { IYesNoDialogProps } from 'components/dialog/YesNoDialog'; -import React, { createContext, useState } from 'react'; +import React, { createContext, ReactNode, useState } from 'react'; export interface IDialogContext { /** @@ -53,10 +55,11 @@ export interface IDialogContext { } export interface ISnackbarProps { - snackbarText: string; open: boolean; onClose: () => void; - severity: Color; + severity?: Color; + color?: Color; + snackbarMessage: ReactNode; } export const defaultYesNoDialogProps: IYesNoDialogProps = { @@ -87,12 +90,11 @@ export const defaultErrorDialogProps: IErrorDialogProps = { }; export const defaultSnackbarProps: ISnackbarProps = { - snackbarText: '', + snackbarMessage: '', open: false, onClose: () => { // default do nothing - }, - severity: 'info' + } }; export const DialogContext = createContext({ @@ -148,11 +150,23 @@ export const DialogContextProvider: React.FC = (props) => { {props.children} - setSnackbar({ open: false })}> - - {snackbarProps.snackbarText} - - + setSnackbar({ open: false })} + message={snackbarProps.snackbarMessage} + action={ + + setSnackbar({ open: false })}> + + + + } + /> ); }; diff --git a/app/src/features/admin/AdminUsersRouter.tsx b/app/src/features/admin/AdminUsersRouter.tsx index b937b303c8..d10d65713b 100644 --- a/app/src/features/admin/AdminUsersRouter.tsx +++ b/app/src/features/admin/AdminUsersRouter.tsx @@ -2,31 +2,30 @@ import AdminUsersLayout from 'features/admin/AdminUsersLayout'; import React from 'react'; import { Redirect, Switch } from 'react-router'; import AppRoute from 'utils/AppRoute'; -import PrivateRoute from 'utils/PrivateRoute'; import ManageUsersPage from './users/ManageUsersPage'; - -interface IAdminUsersRouterProps { - classes: any; -} +import UsersDetailPage from './users/UsersDetailPage'; /** - * Router for all `/admin/users*` pages. + * Router for all `/admin/users/*` pages. * * @param {*} props * @return {*} */ -const AdminUsersRouter: React.FC = (props) => { +const AdminUsersRouter: React.FC = (props) => { return ( - + + + + + + + + {/* Catch any unknown routes, and re-direct to the not found page */} - } /> + + + ); }; diff --git a/app/src/features/admin/users/AccessRequestList.test.tsx b/app/src/features/admin/users/AccessRequestList.test.tsx index a19637eda8..b389bc9208 100644 --- a/app/src/features/admin/users/AccessRequestList.test.tsx +++ b/app/src/features/admin/users/AccessRequestList.test.tsx @@ -1,15 +1,17 @@ -import { codes } from 'test-helpers/code-helpers'; import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import AccessRequestList from 'features/admin/users/AccessRequestList'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; import { IAccessRequestDataObject, IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import React from 'react'; -import { useBiohubApi } from 'hooks/useBioHubApi'; +import { codes } from 'test-helpers/code-helpers'; jest.mock('../../../hooks/useBioHubApi'); const mockUseBiohubApi = { admin: { - updateAccessRequest: jest.fn() + approveAccessRequest: jest.fn(), + denyAccessRequest: jest.fn() } }; @@ -28,7 +30,8 @@ const renderContainer = ( describe('AccessRequestList', () => { beforeEach(() => { // clear mocks before each test - mockBiohubApi().admin.updateAccessRequest.mockClear(); + mockBiohubApi().admin.approveAccessRequest.mockClear(); + mockBiohubApi().admin.denyAccessRequest.mockClear(); }); afterEach(() => { @@ -59,10 +62,9 @@ describe('AccessRequestList', () => { username: 'testusername', email: 'email@email.com', role: 2, - identitySource: 'idir', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, company: 'test company', - regional_offices: [1, 2], - comments: 'test comment' + reason: 'my reason' }, create_date: '2020-04-20' } @@ -72,11 +74,9 @@ describe('AccessRequestList', () => { ); await waitFor(() => { - expect(getByText('test user')).toBeVisible(); expect(getByText('testusername')).toBeVisible(); - expect(getByText('test company')).toBeVisible(); - expect(getByText('April-20-2020')).toBeVisible(); - expect(getByText('PENDING')).toBeVisible(); + expect(getByText('Apr 20, 2020')).toBeVisible(); + expect(getByText('Pending')).toBeVisible(); expect(getByRole('button')).toHaveTextContent('Review'); }); }); @@ -97,10 +97,9 @@ describe('AccessRequestList', () => { username: 'testusername', email: 'email@email.com', role: 2, - identitySource: 'idir', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, company: 'test company', - regional_offices: [1, 2], - comments: 'test comment' + reason: 'my reason' }, create_date: '2020-04-20' } @@ -110,11 +109,9 @@ describe('AccessRequestList', () => { ); await waitFor(() => { - expect(getByText('test user')).toBeVisible(); expect(getByText('testusername')).toBeVisible(); - expect(getByText('test company')).toBeVisible(); - expect(getByText('April-20-2020')).toBeVisible(); - expect(getByText('DENIED')).toBeVisible(); + expect(getByText('Apr 20, 2020')).toBeVisible(); + expect(getByText('Denied')).toBeVisible(); expect(queryByRole('button')).not.toBeInTheDocument(); }); }); @@ -135,10 +132,9 @@ describe('AccessRequestList', () => { username: 'testusername', email: 'email@email.com', role: 2, - identitySource: 'idir', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, company: 'test company', - regional_offices: [1, 2], - comments: 'test comment' + reason: 'my reason' }, create_date: '2020-04-20' } @@ -148,17 +144,15 @@ describe('AccessRequestList', () => { ); await waitFor(() => { - expect(getByText('test user')).toBeVisible(); expect(getByText('testusername')).toBeVisible(); - expect(getByText('test company')).toBeVisible(); - expect(getByText('April-20-2020')).toBeVisible(); - expect(getByText('APPROVED')).toBeVisible(); + expect(getByText('Apr 20, 2020')).toBeVisible(); + expect(getByText('Approved')).toBeVisible(); expect(queryByRole('button')).not.toBeInTheDocument(); }); }); it('shows a table row when the data object is null', async () => { - const { getByText, getAllByText } = renderContainer( + const { getByText } = renderContainer( [ { id: 1, @@ -177,16 +171,15 @@ describe('AccessRequestList', () => { ); await waitFor(() => { - expect(getAllByText('Not Applicable').length).toEqual(2); - expect(getByText('April-20-2020')).toBeVisible(); - expect(getByText('PENDING')).toBeVisible(); + expect(getByText('Apr 20, 2020')).toBeVisible(); + expect(getByText('Pending')).toBeVisible(); }); }); - it('opens the review dialog and calls updateAccessRequest on approval', async () => { + it('opens the review dialog and calls approveAccessRequest on approval', async () => { const refresh = jest.fn(); - const { getByText, getByRole } = renderContainer( + const { getByText, getByRole, getByTestId } = renderContainer( [ { id: 1, @@ -201,10 +194,9 @@ describe('AccessRequestList', () => { username: 'testusername', email: 'email@email.com', role: 2, - identitySource: 'idir', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, company: 'test company', - regional_offices: [1, 2], - comments: 'test comment' + reason: 'my reason' }, create_date: '2020-04-20' } @@ -220,20 +212,25 @@ describe('AccessRequestList', () => { await waitFor(() => { // wait for dialog to open expect(getByText('Review Access Request')).toBeVisible(); - fireEvent.click(getByText('Approve')); + fireEvent.click(getByTestId('request_approve_button')); }); await waitFor(() => { expect(refresh).toHaveBeenCalledTimes(1); - expect(mockBiohubApi().admin.updateAccessRequest).toHaveBeenCalledTimes(1); - expect(mockBiohubApi().admin.updateAccessRequest).toHaveBeenCalledWith('testusername', 'idir', 1, 2, [2]); + expect(mockBiohubApi().admin.approveAccessRequest).toHaveBeenCalledTimes(1); + expect(mockBiohubApi().admin.approveAccessRequest).toHaveBeenCalledWith( + 1, + 'testusername', + SYSTEM_IDENTITY_SOURCE.IDIR, + [2] + ); }); }); - it('opens the review dialog and calls updateAccessRequest on denial', async () => { + it('opens the review dialog and calls denyAccessRequest on denial', async () => { const refresh = jest.fn(); - const { getByText, getByRole } = renderContainer( + const { getByText, getByRole, getByTestId } = renderContainer( [ { id: 1, @@ -248,10 +245,9 @@ describe('AccessRequestList', () => { username: 'testusername', email: 'email@email.com', role: 1, - identitySource: 'idir', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, company: 'test company', - regional_offices: [1, 2], - comments: 'test comment' + reason: 'my reason' }, create_date: '2020-04-20' } @@ -267,13 +263,13 @@ describe('AccessRequestList', () => { await waitFor(() => { // wait for dialog to open expect(getByText('Review Access Request')).toBeVisible(); - fireEvent.click(getByText('Deny')); + fireEvent.click(getByTestId('request_deny_button')); }); await waitFor(() => { expect(refresh).toHaveBeenCalledTimes(1); - expect(mockBiohubApi().admin.updateAccessRequest).toHaveBeenCalledTimes(1); - expect(mockBiohubApi().admin.updateAccessRequest).toHaveBeenCalledWith('testusername', 'idir', 1, 3); + expect(mockBiohubApi().admin.denyAccessRequest).toHaveBeenCalledTimes(1); + expect(mockBiohubApi().admin.denyAccessRequest).toHaveBeenCalledWith(1); }); }); }); diff --git a/app/src/features/admin/users/AccessRequestList.tsx b/app/src/features/admin/users/AccessRequestList.tsx index 3f542a2f9b..d99f91cdd9 100644 --- a/app/src/features/admin/users/AccessRequestList.tsx +++ b/app/src/features/admin/users/AccessRequestList.tsx @@ -1,8 +1,6 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; -import Chip from '@material-ui/core/Chip'; import Paper from '@material-ui/core/Paper'; -import { Theme } from '@material-ui/core/styles/createMuiTheme'; import makeStyles from '@material-ui/core/styles/makeStyles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; @@ -10,13 +8,15 @@ import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; +import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; -import clsx from 'clsx'; +import { AccessStatusChip } from 'components/chips/RequestChips'; import RequestDialog from 'components/dialog/RequestDialog'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { ReviewAccessRequestI18N } from 'constants/i18n'; import { AdministrativeActivityStatusType } from 'constants/misc'; import { DialogContext } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; @@ -28,25 +28,11 @@ import ReviewAccessRequestForm, { ReviewAccessRequestFormYupSchema } from './ReviewAccessRequestForm'; -const useStyles = makeStyles((theme: Theme) => ({ - chip: { - padding: '0px 8px', - borderRadius: '4px', - color: 'white' - }, - chipPending: { - backgroundColor: theme.palette.primary.main - }, - chipActioned: { - backgroundColor: theme.palette.success.main - }, - chipRejected: { - backgroundColor: theme.palette.error.main - }, - actionButton: { - minWidth: '6rem', - '& + button': { - marginLeft: '0.5rem' +const useStyles = makeStyles(() => ({ + table: { + tableLayout: 'fixed', + '& td': { + verticalAlign: 'middle' } } })); @@ -70,13 +56,6 @@ const AccessRequestList: React.FC = (props) => { const biohubApi = useBiohubApi(); - const approvedCodeId = codes?.administrative_activity_status_type.find( - (item) => item.name === AdministrativeActivityStatusType.ACTIONED - )?.id as any; - const rejectedCodeId = codes?.administrative_activity_status_type.find( - (item) => item.name === AdministrativeActivityStatusType.REJECTED - )?.id as any; - const [activeReviewDialog, setActiveReviewDialog] = useState<{ open: boolean; request: IGetAccessRequestsListResponse | any; @@ -105,17 +84,20 @@ const AccessRequestList: React.FC = (props) => { setActiveReviewDialog({ open: false, request: null }); try { - await biohubApi.admin.updateAccessRequest( + await biohubApi.admin.approveAccessRequest( + updatedRequest.id, updatedRequest.data.username, updatedRequest.data.identitySource, - updatedRequest.id, - approvedCodeId, - values.system_roles + (values.system_role && [values.system_role]) || [] ); refresh(); } catch (error) { - dialogContext.setErrorDialog({ ...defaultErrorDialogProps, open: true, dialogErrorDetails: error }); + dialogContext.setErrorDialog({ + ...defaultErrorDialogProps, + open: true, + dialogErrorDetails: (error as APIError).errors + }); } }; @@ -125,37 +107,18 @@ const AccessRequestList: React.FC = (props) => { setActiveReviewDialog({ open: false, request: null }); try { - await biohubApi.admin.updateAccessRequest( - updatedRequest.data.username, - updatedRequest.data.identitySource, - updatedRequest.id, - rejectedCodeId - ); + await biohubApi.admin.denyAccessRequest(updatedRequest.id); refresh(); } catch (error) { - dialogContext.setErrorDialog({ ...defaultErrorDialogProps, open: true, dialogErrorDetails: error }); + dialogContext.setErrorDialog({ + ...defaultErrorDialogProps, + open: true, + dialogErrorDetails: (error as APIError).errors + }); } }; - const getChipIcon = (status_name: string) => { - let chipLabel; - let chipStatusClass; - - if (AdministrativeActivityStatusType.REJECTED === status_name) { - chipLabel = 'DENIED'; - chipStatusClass = classes.chipRejected; - } else if (AdministrativeActivityStatusType.ACTIONED === status_name) { - chipLabel = 'APPROVED'; - chipStatusClass = classes.chipActioned; - } else { - chipLabel = 'PENDING'; - chipStatusClass = classes.chipPending; - } - - return ; - }; - return ( <> = (props) => { component={{ initialValues: { ...ReviewAccessRequestFormInitialValues, - system_roles: [activeReviewDialog.request?.data?.role] + system_role: activeReviewDialog.request?.data?.role }, validationSchema: ReviewAccessRequestFormYupSchema, element: ( @@ -178,58 +141,52 @@ const AccessRequestList: React.FC = (props) => { return { value: item.id, label: item.name }; }) || [] } - regional_offices={codes?.regional_offices} /> ) }} /> - - Access Requests ({accessRequests?.length || 0}) - + + + Access Requests ({accessRequests?.length || 0}) + + -
+
- Name Username - Company - Regional Offices - Request Date - Status - + Date of Request + Access Status + + Actions + {!accessRequests?.length && ( - + No Access Requests )} {accessRequests?.map((row, index) => { - const regional_offices = row.data?.regional_offices - ?.map((regionId) => codes.regional_offices.find((code) => code.id === regionId)?.name) - .join(', '); - return ( - {row.data?.name || ''} {row.data?.username || ''} - {row.data?.company || 'Not Applicable'} - {regional_offices || 'Not Applicable'} - {getFormattedDate(DATE_FORMAT.MediumDateFormat2, row.create_date)} - {getChipIcon(row.status_name)} - + {getFormattedDate(DATE_FORMAT.ShortMediumDateFormat, row.create_date)} + + + + {row.status_name === AdministrativeActivityStatusType.PENDING && ( )} diff --git a/app/src/features/admin/users/ActiveUsersList.test.tsx b/app/src/features/admin/users/ActiveUsersList.test.tsx index 85df60504d..102f5b69c0 100644 --- a/app/src/features/admin/users/ActiveUsersList.test.tsx +++ b/app/src/features/admin/users/ActiveUsersList.test.tsx @@ -1,15 +1,27 @@ import { render, waitFor } from '@testing-library/react'; -import { IGetUserResponse } from 'interfaces/useUserApi.interface'; +import { createMemoryHistory } from 'history'; import React from 'react'; -import ActiveUsersList from './ActiveUsersList'; +import { Router } from 'react-router'; +import { codes } from 'test-helpers/code-helpers'; +import ActiveUsersList, { IActiveUsersListProps } from './ActiveUsersList'; -const renderContainer = (activeUsers: IGetUserResponse[]) => { - return render(); +const history = createMemoryHistory(); + +const renderContainer = (props: IActiveUsersListProps) => { + return render( + + + + ); }; describe('ActiveUsersList', () => { it('shows `No Active Users` when there are no active users', async () => { - const { getByText } = renderContainer([]); + const { getByText } = renderContainer({ + activeUsers: [], + codes: codes, + refresh: () => {} + }); await waitFor(() => { expect(getByText('No Active Users')).toBeVisible(); @@ -17,13 +29,18 @@ describe('ActiveUsersList', () => { }); it('shows a table row for an active user with all fields having values', async () => { - const { getByText } = renderContainer([ - { - id: 1, - user_identifier: 'username', - role_names: ['role 1', 'role 2'] - } - ]); + const { getByText } = renderContainer({ + activeUsers: [ + { + id: 1, + user_identifier: 'username', + user_record_end_date: '2020-10-10', + role_names: ['role 1', 'role 2'] + } + ], + codes: codes, + refresh: () => {} + }); await waitFor(() => { expect(getByText('username')).toBeVisible(); @@ -32,16 +49,33 @@ describe('ActiveUsersList', () => { }); it('shows a table row for an active user with fields not having values', async () => { - const { getAllByText } = renderContainer([ - { - id: 1, - user_identifier: '', - role_names: [] - } - ]); + const { getByTestId } = renderContainer({ + activeUsers: [ + { + id: 1, + user_identifier: 'username', + user_record_end_date: '2020-10-10', + role_names: [] + } + ], + codes: codes, + refresh: () => {} + }); + + await waitFor(() => { + expect(getByTestId('custom-menu-button-NotApplicable')).toBeInTheDocument(); + }); + }); + + it('renders the add new users button correctly', async () => { + const { getByTestId } = renderContainer({ + activeUsers: [], + codes: codes, + refresh: () => {} + }); await waitFor(() => { - expect(getAllByText('Not Applicable').length).toEqual(2); + expect(getByTestId('invite-system-users-button')).toBeVisible(); }); }); }); diff --git a/app/src/features/admin/users/ActiveUsersList.tsx b/app/src/features/admin/users/ActiveUsersList.tsx index 920307d871..90f7037156 100644 --- a/app/src/features/admin/users/ActiveUsersList.tsx +++ b/app/src/features/admin/users/ActiveUsersList.tsx @@ -1,5 +1,9 @@ import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import Grid from '@material-ui/core/Grid'; import Paper from '@material-ui/core/Paper'; +import { Theme } from '@material-ui/core/styles/createMuiTheme'; +import makeStyles from '@material-ui/core/styles/makeStyles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; @@ -7,13 +11,41 @@ import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TablePagination from '@material-ui/core/TablePagination'; import TableRow from '@material-ui/core/TableRow'; +import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; +import { mdiDotsVertical, mdiInformationOutline, mdiMenuDown, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import EditDialog from 'components/dialog/EditDialog'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { CustomMenuButton, CustomMenuIconButton } from 'components/toolbar/ActionToolbars'; +import { DeleteSystemUserI18N } from 'constants/i18n'; +import { DialogContext, ISnackbarProps } from 'contexts/dialogContext'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import { IGetUserResponse } from 'interfaces/useUserApi.interface'; -import React, { useState } from 'react'; -import { handleChangeRowsPerPage, handleChangePage } from 'utils/tablePaginationUtils'; +import React, { useContext, useState } from 'react'; +import { useHistory } from 'react-router'; +import { handleChangePage, handleChangeRowsPerPage } from 'utils/tablePaginationUtils'; +import AddSystemUsersForm, { + AddSystemUsersFormInitialValues, + AddSystemUsersFormYupSchema, + IAddSystemUsersForm +} from './AddSystemUsersForm'; + +const useStyles = makeStyles((theme: Theme) => ({ + table: { + tableLayout: 'fixed', + '& td': { + verticalAlign: 'middle' + } + } +})); export interface IActiveUsersListProps { activeUsers: IGetUserResponse[]; + codes: IGetAllCodeSetsResponse; + refresh: () => void; } /** @@ -23,64 +55,315 @@ export interface IActiveUsersListProps { * @return {*} */ const ActiveUsersList: React.FC = (props) => { - const { activeUsers } = props; + const classes = useStyles(); + const biohubApi = useBiohubApi(); + const { activeUsers, codes } = props; + const history = useHistory(); - const [rowsPerPage, setRowsPerPage] = useState(5); + const [rowsPerPage, setRowsPerPage] = useState(20); const [page, setPage] = useState(0); + const dialogContext = useContext(DialogContext); + + const [openAddUserDialog, setOpenAddUserDialog] = useState(false); + + const defaultErrorDialogProps = { + dialogTitle: DeleteSystemUserI18N.deleteErrorTitle, + dialogText: DeleteSystemUserI18N.deleteErrorText, + open: false, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }; + + const showErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ ...defaultErrorDialogProps, ...textDialogProps, open: true }); + }; + + const showSnackBar = (textDialogProps?: Partial) => { + dialogContext.setSnackbar({ ...textDialogProps, open: true }); + }; + + const handleRemoveUserClick = (row: IGetUserResponse) => { + dialogContext.setYesNoDialog({ + dialogTitle: 'Remove User?', + dialogContent: ( + + Removing user {row.user_identifier} will revoke their access to this application and all + related projects. Are you sure you want to proceed? + + ), + yesButtonLabel: 'Remove User', + noButtonLabel: 'Cancel', + yesButtonProps: { color: 'secondary' }, + onClose: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onNo: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + open: true, + onYes: () => { + deActivateSystemUser(row); + dialogContext.setYesNoDialog({ open: false }); + } + }); + }; + + const deActivateSystemUser = async (user: IGetUserResponse) => { + if (!user?.id) { + return; + } + try { + await biohubApi.user.deleteSystemUser(user.id); + + showSnackBar({ + snackbarMessage: ( + <> + + User {user.user_identifier} removed from application. + + + ), + open: true + }); + + props.refresh(); + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, dialogErrorDetails: apiError.errors, open: true }); + } + }; + + const handleChangeUserPermissionsClick = (row: IGetUserResponse, newRoleName: any, newRoleId: number) => { + dialogContext.setYesNoDialog({ + dialogTitle: 'Change User Role?', + dialogContent: ( + + Change user {row.user_identifier}'s role to {newRoleName}? + + ), + yesButtonLabel: 'Change Role', + noButtonLabel: 'Cancel', + yesButtonProps: { color: 'primary' }, + onClose: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onNo: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + open: true, + onYes: () => { + changeSystemUserRole(row, newRoleId, newRoleName); + dialogContext.setYesNoDialog({ open: false }); + } + }); + }; + + const changeSystemUserRole = async (user: IGetUserResponse, roleId: number, roleName: string) => { + if (!user?.id) { + return; + } + const roleIds = [roleId]; + + try { + await biohubApi.user.updateSystemUserRoles(user.id, roleIds); + + showSnackBar({ + snackbarMessage: ( + <> + + User {user.user_identifier}'s role has changed to {roleName}. + + + ), + open: true + }); + + props.refresh(); + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, dialogErrorDetails: apiError.errors, open: true }); + } + }; + + const handleAddSystemUsersSave = async (values: IAddSystemUsersForm) => { + setOpenAddUserDialog(false); + + try { + for (const systemUser of values.systemUsers) { + await biohubApi.admin.addSystemUser( + systemUser.userIdentifier, + systemUser.identitySource, + systemUser.system_role + ); + } + + props.refresh(); + + dialogContext.setSnackbar({ + open: true, + snackbarMessage: ( + + {values.systemUsers.length} system {values.systemUsers.length > 1 ? 'users' : 'user'} added. + + ) + }); + } catch (error) { + dialogContext.setErrorDialog({ + ...defaultErrorDialogProps, + open: true, + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError).errors + }); + } + }; return ( - - - Active Users ({activeUsers?.length || 0}) - - -
- - - Name - Username - Company - Regional Offices - Roles - Last Active - - - - {!activeUsers?.length && ( - - - No Active Users + <> + + + + + + Active Users ({activeUsers?.length || 0}) + + + + + + + + + + + +
+ + + Username + Role + + Actions - )} - {activeUsers.length > 0 && - activeUsers.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => ( - - - {row.user_identifier || 'Not Applicable'} - - - {row.role_names.join(', ') || 'Not Applicable'} - + + + {!activeUsers?.length && ( + + + No Active Users + - ))} - -
-
- {activeUsers?.length > 0 && ( - handleChangePage(event, newPage, setPage)} - onChangeRowsPerPage={(event: React.ChangeEvent) => - handleChangeRowsPerPage(event, setPage, setRowsPerPage) - } - /> - )} -
+ )} + {activeUsers.length > 0 && + activeUsers.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => ( + + + {row.user_identifier || 'Not Applicable'} + + + + { + return item1.name.localeCompare(item2.name); + }) + .map((item) => { + return { + menuLabel: item.name, + menuOnClick: () => handleChangeUserPermissionsClick(row, item.name, item.id) + }; + })} + buttonEndIcon={} + /> + + + + + } + menuItems={[ + { + menuIcon: , + menuLabel: 'View Users Details', + menuOnClick: () => + history.push({ + pathname: `/admin/users/${row.id}`, + state: row + }) + }, + { + menuIcon: , + menuLabel: 'Remove User', + menuOnClick: () => handleRemoveUserClick(row) + } + ]} + /> + + + + ))} + + + + {activeUsers?.length > 0 && ( + handleChangePage(event, newPage, setPage)} + onChangeRowsPerPage={(event: React.ChangeEvent) => + handleChangeRowsPerPage(event, setPage, setRowsPerPage) + } + /> + )} + + + { + return { value: item.id, label: item.name }; + }) || [] + } + /> + ), + initialValues: AddSystemUsersFormInitialValues, + validationSchema: AddSystemUsersFormYupSchema + }} + onCancel={() => setOpenAddUserDialog(false)} + onSave={(values) => { + handleAddSystemUsersSave(values); + setOpenAddUserDialog(false); + }} + /> + ); }; diff --git a/app/src/features/admin/users/AddSystemUsersForm.tsx b/app/src/features/admin/users/AddSystemUsersForm.tsx new file mode 100644 index 0000000000..ca7869beda --- /dev/null +++ b/app/src/features/admin/users/AddSystemUsersForm.tsx @@ -0,0 +1,166 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import InputLabel from '@material-ui/core/InputLabel'; +import MenuItem from '@material-ui/core/MenuItem'; +import Select from '@material-ui/core/Select'; +import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import CustomTextField from 'components/fields/CustomTextField'; +import { FieldArray, useFormikContext } from 'formik'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; +import React from 'react'; +import yup from 'utils/YupSchema'; + +export interface IAddSystemUsersFormArrayItem { + userIdentifier: string; + identitySource: string; + system_role: number; +} + +export interface IAddSystemUsersForm { + systemUsers: IAddSystemUsersFormArrayItem[]; +} + +export const AddSystemUsersFormArrayItemInitialValues: IAddSystemUsersFormArrayItem = { + userIdentifier: '', + identitySource: '', + system_role: ('' as unknown) as number +}; + +export const AddSystemUsersFormInitialValues: IAddSystemUsersForm = { + systemUsers: [AddSystemUsersFormArrayItemInitialValues] +}; + +export const AddSystemUsersFormYupSchema = yup.object().shape({ + systemUsers: yup.array().of( + yup.object().shape({ + userIdentifier: yup.string().required('Username is required'), + identitySource: yup.string().required('Login Method is required'), + system_role: yup.number().required('Role is required') + }) + ) +}); + +export interface AddSystemUsersFormProps { + system_roles: any[]; +} + +const AddSystemUsersForm: React.FC = (props) => { + const { values, handleChange, handleSubmit, getFieldMeta } = useFormikContext(); + + return ( +
+ ( + + + {values.systemUsers?.map((systemUser, index) => { + const userIdentifierMeta = getFieldMeta(`systemUsers.[${index}].userIdentifier`); + const identitySourceMeta = getFieldMeta(`systemUsers.[${index}].identitySource`); + const systemRoleMeta = getFieldMeta(`systemUsers.[${index}].roleId`); + + return ( + + + + + + + + + Login Method + + + {identitySourceMeta.touched && identitySourceMeta.error} + + + + + + System Role + + + {systemRoleMeta.touched && systemRoleMeta.error} + + + + arrayHelpers.remove(index)}> + + + + + + ); + })} + + + + + + )} + /> + + ); +}; + +export default AddSystemUsersForm; diff --git a/app/src/features/admin/users/ManageUsersPage.test.tsx b/app/src/features/admin/users/ManageUsersPage.test.tsx index de33d59b0a..5feda48d04 100644 --- a/app/src/features/admin/users/ManageUsersPage.test.tsx +++ b/app/src/features/admin/users/ManageUsersPage.test.tsx @@ -1,16 +1,24 @@ import { cleanup, render, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; +import { Router } from 'react-router'; import ManageUsersPage from './ManageUsersPage'; +const history = createMemoryHistory(); + const renderContainer = () => { - return render(); + return render( + + + + ); }; jest.mock('../../../hooks/useBioHubApi'); const mockUseBiohubApi = { admin: { - getAccessRequests: jest.fn() + getAdministrativeActivities: jest.fn() }, user: { getUsersList: jest.fn() @@ -27,7 +35,7 @@ const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock { beforeEach(() => { // clear mocks before each test - mockBiohubApi().admin.getAccessRequests.mockClear(); + mockBiohubApi().admin.getAdministrativeActivities.mockClear(); mockBiohubApi().user.getUsersList.mockClear(); mockBiohubApi().codes.getAllCodeSets.mockClear(); @@ -46,7 +54,7 @@ describe('ManageUsersPage', () => { }); it('renders the main page content correctly', async () => { - mockBiohubApi().admin.getAccessRequests.mockReturnValue([]); + mockBiohubApi().admin.getAdministrativeActivities.mockReturnValue([]); mockBiohubApi().user.getUsersList.mockReturnValue([]); const { getByText } = renderContainer(); @@ -57,7 +65,7 @@ describe('ManageUsersPage', () => { }); it('renders the access requests and active users component', async () => { - mockBiohubApi().admin.getAccessRequests.mockReturnValue([]); + mockBiohubApi().admin.getAdministrativeActivities.mockReturnValue([]); mockBiohubApi().user.getUsersList.mockReturnValue([]); const { getByText } = renderContainer(); diff --git a/app/src/features/admin/users/ManageUsersPage.tsx b/app/src/features/admin/users/ManageUsersPage.tsx index 8221d5e3ea..1587c97ec5 100644 --- a/app/src/features/admin/users/ManageUsersPage.tsx +++ b/app/src/features/admin/users/ManageUsersPage.tsx @@ -2,7 +2,7 @@ import Box from '@material-ui/core/Box'; import CircularProgress from '@material-ui/core/CircularProgress'; import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; -import { AdministrativeActivityStatusType } from 'constants/misc'; +import { AdministrativeActivityStatusType, AdministrativeActivityType } from 'constants/misc'; import AccessRequestList from 'features/admin/users/AccessRequestList'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; @@ -31,20 +31,20 @@ const ManageUsersPage: React.FC = () => { const [isLoadingCodes, setIsLoadingCodes] = useState(false); const refreshAccessRequests = async () => { - const accessResponse = await biohubApi.admin.getAccessRequests([ - AdministrativeActivityStatusType.PENDING, - AdministrativeActivityStatusType.REJECTED - ]); + const accessResponse = await biohubApi.admin.getAdministrativeActivities( + [AdministrativeActivityType.SYSTEM_ACCESS], + [AdministrativeActivityStatusType.PENDING, AdministrativeActivityStatusType.REJECTED] + ); setAccessRequests(accessResponse); }; useEffect(() => { const getAccessRequests = async () => { - const accessResponse = await biohubApi.admin.getAccessRequests([ - AdministrativeActivityStatusType.PENDING, - AdministrativeActivityStatusType.REJECTED - ]); + const accessResponse = await biohubApi.admin.getAdministrativeActivities( + [AdministrativeActivityType.SYSTEM_ACCESS], + [AdministrativeActivityStatusType.PENDING, AdministrativeActivityStatusType.REJECTED] + ); setAccessRequests(() => { setHasLoadedAccessRequests(true); @@ -122,6 +122,7 @@ const ManageUsersPage: React.FC = () => { Manage Users + { /> - + diff --git a/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx b/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx index 4b7cd0d1be..3f783186d3 100644 --- a/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx +++ b/app/src/features/admin/users/ReviewAccessRequestForm.test.tsx @@ -1,66 +1,111 @@ import { render, waitFor } from '@testing-library/react'; import ReviewAccessRequestForm, { + ReviewAccessRequestFormInitialValues, ReviewAccessRequestFormYupSchema } from 'features/admin/users/ReviewAccessRequestForm'; import { Formik } from 'formik'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import React from 'react'; import { codes } from 'test-helpers/code-helpers'; -const renderContainer = (accessRequest: IGetAccessRequestsListResponse) => { - return render( - {}}> - {() => ( - { - return { value: item.id, label: item.name }; - })} - regional_offices={codes.regional_offices} - /> - )} - - ); -}; - describe('ReviewAccessRequestForm', () => { - it('renders all fields from the request object', async () => { - const { getByText } = renderContainer({ - id: 1, - type: 2, - type_name: 'test type name', - status: 3, - status_name: 'test status name', - description: 'test description', - notes: 'test node', - create_date: '2021-04-18', - data: { - name: 'test data name', - username: 'test data username', - email: 'test data email', - identitySource: 'idir', - role: 2, - company: 'test data company', - regional_offices: [1], - comments: 'test data comment', - request_reason: 'reason for request' - } + describe('IDIR Request', () => { + it('renders all fields from the request object', async () => { + const requestData: IGetAccessRequestsListResponse = { + id: 1, + type: 2, + type_name: 'test type name', + status: 3, + status_name: 'test status name', + description: 'test description', + notes: 'test node', + create_date: '2021-04-18', + data: { + name: 'test data name', + username: 'test data username', + email: 'test data email', + identitySource: SYSTEM_IDENTITY_SOURCE.IDIR, + role: 2, + reason: 'reason for request' + } + }; + + const { getByText } = render( + {}}> + {() => ( + { + return { value: item.id, label: item.name }; + })} + /> + )} + + ); + + await waitFor(() => { + expect(getByText('test data name')).toBeVisible(); + expect(getByText('IDIR/test data username')).toBeVisible(); + expect(getByText('test data email')).toBeVisible(); + expect(getByText('04/18/2021')).toBeVisible(); + }); }); + }); + + describe('BCeID Request', () => { + it('renders all fields from the request object', async () => { + const requestData: IGetAccessRequestsListResponse = { + id: 1, + type: 2, + type_name: 'test type name', + status: 3, + status_name: 'test status name', + description: 'test description', + notes: 'test node', + create_date: '2021-04-18', + data: { + name: 'test data name', + username: 'test data username', + email: 'test data email', + identitySource: SYSTEM_IDENTITY_SOURCE.BCEID, + company: 'test company', + reason: 'reason for request' + } + }; + + const { getByText } = render( + {}}> + {() => ( + { + return { value: item.id, label: item.name }; + })} + /> + )} + + ); - await waitFor(() => { - expect(getByText('test data name')).toBeVisible(); - expect(getByText('IDIR/test data username')).toBeVisible(); - expect(getByText('test data email')).toBeVisible(); - expect(getByText('Office 1')).toBeVisible(); - expect(getByText('04/18/2021')).toBeVisible(); - expect(getByText('test data comment')).toBeVisible(); - expect(getByText('Role 2')).toBeVisible(); + await waitFor(() => { + expect(getByText('test data name')).toBeVisible(); + expect(getByText('BCEID/test data username')).toBeVisible(); + expect(getByText('test data email')).toBeVisible(); + expect(getByText('04/18/2021')).toBeVisible(); + expect(getByText('test company')).toBeVisible(); + }); }); }); }); diff --git a/app/src/features/admin/users/ReviewAccessRequestForm.tsx b/app/src/features/admin/users/ReviewAccessRequestForm.tsx index 8b26189c2c..232f3acf4c 100644 --- a/app/src/features/admin/users/ReviewAccessRequestForm.tsx +++ b/app/src/features/admin/users/ReviewAccessRequestForm.tsx @@ -1,33 +1,29 @@ import Box from '@material-ui/core/Box'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; -import MultiAutocompleteFieldVariableSize, { - IMultiAutocompleteFieldOption -} from 'components/fields/MultiAutocompleteFieldVariableSize'; +import AutocompleteField, { IAutocompleteFieldOption } from 'components/fields/AutocompleteField'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { useFormikContext } from 'formik'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; -import { CodeSet, ICode } from 'interfaces/useCodesApi.interface'; import React from 'react'; import { getFormattedDate } from 'utils/Utils'; import yup from 'utils/YupSchema'; export interface IReviewAccessRequestForm { - system_roles: number[]; + system_role: number; } export const ReviewAccessRequestFormInitialValues: IReviewAccessRequestForm = { - system_roles: [] + system_role: ('' as unknown) as number }; export const ReviewAccessRequestFormYupSchema = yup.object().shape({ - system_roles: yup.array().of(yup.number()).min(1, 'Required').required('Required') + system_role: yup.number().nullable().notRequired() }); export interface IReviewAccessRequestFormProps { request: IGetAccessRequestsListResponse; - system_roles: IMultiAutocompleteFieldOption[]; - regional_offices: CodeSet; + system_roles: IAutocompleteFieldOption[]; } /** @@ -38,14 +34,6 @@ export interface IReviewAccessRequestFormProps { const ReviewAccessRequestForm: React.FC = (props) => { const { handleSubmit } = useFormikContext(); - const regional_offices = - props.request.data.regional_offices - ?.map((regionId) => props.regional_offices.find((code) => code.id === regionId)?.name) - .join(', ') || 'Not Applicable'; - - const company = props.request.data.company || 'Not Applicable'; - const request_reason = props.request.data.request_reason || 'Not Applicable'; - return ( @@ -54,7 +42,7 @@ const ReviewAccessRequestForm: React.FC = (props)
- + Name @@ -62,15 +50,7 @@ const ReviewAccessRequestForm: React.FC = (props) {props.request.data.name} - - - Username - - - {props.request.data.identitySource.toUpperCase()}/{props.request.data.username} - - - + Email Address @@ -78,24 +58,17 @@ const ReviewAccessRequestForm: React.FC = (props) {props.request.data.email} - - - Regional Offices - - - {regional_offices} - - - + + - Company + Username - {company} + {props.request.data.identitySource.toUpperCase()}/{props.request.data.username} - + Request Date @@ -103,12 +76,13 @@ const ReviewAccessRequestForm: React.FC = (props) {getFormattedDate(DATE_FORMAT.ShortDateFormatMonthFirst, props.request.create_date)} - + + - Additional Comments + Company - {props.request.data.comments || 'Not Applicable'} + {('company' in props.request.data && props.request.data.company) || 'Not Applicable'} @@ -117,7 +91,7 @@ const ReviewAccessRequestForm: React.FC = (props) Reason - {request_reason} + {props.request.data.reason} @@ -125,16 +99,16 @@ const ReviewAccessRequestForm: React.FC = (props) - Review / Update Role + Review / Update Requested System Role
- diff --git a/app/src/features/admin/users/UsersDetailHeader.test.tsx b/app/src/features/admin/users/UsersDetailHeader.test.tsx new file mode 100644 index 0000000000..84d0c7f956 --- /dev/null +++ b/app/src/features/admin/users/UsersDetailHeader.test.tsx @@ -0,0 +1,153 @@ +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { DialogContextProvider } from 'contexts/dialogContext'; +import { createMemoryHistory } from 'history'; +import React from 'react'; +import { Router } from 'react-router'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import UsersDetailHeader from './UsersDetailHeader'; + +const history = createMemoryHistory(); + +jest.mock('../../../hooks/useBioHubApi'); + +const mockUseBiohubApi = { + user: { + deleteSystemUser: jest.fn, []>() + } +}; + +const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock).mockReturnValue( + mockUseBiohubApi +); + +const mockUser = { + id: 1, + user_record_end_date: 'ending', + user_identifier: 'testUser', + role_names: ['system'] +}; + +describe('UsersDetailHeader', () => { + afterEach(() => { + cleanup(); + }); + + it('renders correctly when selectedUser are loaded', async () => { + history.push('/admin/users/1'); + + const { getAllByTestId } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + expect(getAllByTestId('remove-user-button').length).toEqual(1); + }); + }); + + it('breadcrumbs link routes user correctly', async () => { + history.push('/admin/users/1'); + + const { getAllByTestId, getByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + }); + + fireEvent.click(getByText('Manage Users')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/users'); + }); + }); + + describe('Are you sure? Dialog', () => { + it('Remove User button opens dialog', async () => { + history.push('/admin/users/1'); + + const { getAllByTestId, getAllByText, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + }); + + fireEvent.click(getByText('Remove User')); + + await waitFor(() => { + expect(getAllByText('Remove System User').length).toEqual(1); + }); + }); + + it('does nothing if the user clicks `No` or away from the dialog', async () => { + history.push('/admin/users/1'); + + const { getAllByTestId, getAllByText, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + }); + + fireEvent.click(getByText('Remove User')); + + await waitFor(() => { + expect(getAllByText('Remove System User').length).toEqual(1); + }); + + fireEvent.click(getByText('No')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/users/1'); + }); + }); + + it('deletes the user and routes user back to Manage Users page', async () => { + mockBiohubApi().user.deleteSystemUser.mockResolvedValue({ + response: 200 + } as any); + + history.push('/admin/users/1'); + + const { getAllByTestId, getAllByText, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + }); + + fireEvent.click(getByText('Remove User')); + + await waitFor(() => { + expect(getAllByText('Remove System User').length).toEqual(1); + }); + + fireEvent.click(getByText('Yes')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/users'); + }); + }); + }); +}); diff --git a/app/src/features/admin/users/UsersDetailHeader.tsx b/app/src/features/admin/users/UsersDetailHeader.tsx new file mode 100644 index 0000000000..07e19e6824 --- /dev/null +++ b/app/src/features/admin/users/UsersDetailHeader.tsx @@ -0,0 +1,180 @@ +import Box from '@material-ui/core/Box'; +import Breadcrumbs from '@material-ui/core/Breadcrumbs'; +import Button from '@material-ui/core/Button'; +import Container from '@material-ui/core/Container'; +import Link from '@material-ui/core/Link'; +import Paper from '@material-ui/core/Paper'; +import makeStyles from '@material-ui/core/styles/makeStyles'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import { mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import React, { useCallback, useContext } from 'react'; +import { useHistory } from 'react-router'; +import { IErrorDialogProps } from '../../../components/dialog/ErrorDialog'; +import { IYesNoDialogProps } from '../../../components/dialog/YesNoDialog'; +import { SystemUserI18N } from '../../../constants/i18n'; +import { DialogContext } from '../../../contexts/dialogContext'; +import { APIError } from '../../../hooks/api/useAxios'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import { IGetUserResponse } from '../../../interfaces/useUserApi.interface'; + +const useStyles = makeStyles(() => ({ + breadCrumbLink: { + display: 'flex', + alignItems: 'center', + cursor: 'pointer' + }, + spacingRight: { + paddingRight: '1rem' + }, + actionButton: { + minWidth: '6rem', + '& + button': { + marginLeft: '0.5rem' + } + }, + projectTitle: { + fontWeight: 400 + } +})); + +export interface IUsersHeaderProps { + userDetails: IGetUserResponse; +} + +const UsersDetailHeader: React.FC = (props) => { + const { userDetails } = props; + const classes = useStyles(); + const history = useHistory(); + const biohubApi = useBiohubApi(); + const dialogContext = useContext(DialogContext); + + const defaultErrorDialogProps: Partial = { + onClose: () => dialogContext.setErrorDialog({ open: false }), + onOk: () => dialogContext.setErrorDialog({ open: false }) + }; + + const defaultYesNoDialogProps: Partial = { + onClose: () => dialogContext.setYesNoDialog({ open: false }), + onNo: () => dialogContext.setYesNoDialog({ open: false }) + }; + + const openYesNoDialog = (yesNoDialogProps?: Partial) => { + dialogContext.setYesNoDialog({ + ...defaultYesNoDialogProps, + ...yesNoDialogProps, + open: true + }); + }; + + const openErrorDialog = useCallback( + (errorDialogProps?: Partial) => { + dialogContext.setErrorDialog({ + ...defaultErrorDialogProps, + ...errorDialogProps, + open: true + }); + }, + [defaultErrorDialogProps, dialogContext] + ); + + const deActivateSystemUser = async (user: IGetUserResponse) => { + if (!user?.id) { + return; + } + try { + await biohubApi.user.deleteSystemUser(user.id); + + dialogContext.setSnackbar({ + snackbarMessage: ( + <> + + User {user.user_identifier} removed from application. + + + ), + open: true + }); + + history.push('/admin/users'); + } catch (error) { + openErrorDialog({ + dialogTitle: SystemUserI18N.removeUserErrorTitle, + dialogText: SystemUserI18N.removeUserErrorText, + dialogError: (error as APIError).message + }); + } + }; + + return ( + + + + + history.push('/admin/users')} + aria-current="page" + className={classes.breadCrumbLink}> + Manage Users + + {userDetails.user_identifier} + + + + + + + User - {userDetails.user_identifier} + + + + + {userDetails.role_names[0]} + + + + + + <> + + + + + + + + ); +}; + +export default UsersDetailHeader; diff --git a/app/src/features/admin/users/UsersDetailPage.test.tsx b/app/src/features/admin/users/UsersDetailPage.test.tsx new file mode 100644 index 0000000000..ce06f7f53f --- /dev/null +++ b/app/src/features/admin/users/UsersDetailPage.test.tsx @@ -0,0 +1,82 @@ +import { cleanup, render, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import React from 'react'; +import { Router } from 'react-router'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; +import { IGetUserResponse } from '../../../interfaces/useUserApi.interface'; +import UsersDetailPage from './UsersDetailPage'; + +const history = createMemoryHistory(); + +jest.mock('../../../hooks/useBioHubApi'); + +const mockUseBiohubApi = { + user: { + getUserById: jest.fn, []>() + }, + codes: { + getAllCodeSets: jest.fn, []>() + }, + project: { + getAllUserProjectsForView: jest.fn, []>() + } +}; + +const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock).mockReturnValue( + mockUseBiohubApi +); + +describe('UsersDetailPage', () => { + beforeEach(() => { + // clear mocks before each test + mockBiohubApi().user.getUserById.mockClear(); + }); + + afterEach(() => { + cleanup(); + }); + + it('shows circular spinner when selectedUser not yet loaded', async () => { + const { getAllByTestId } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('page-loading').length).toEqual(1); + }); + }); + + it('renders correctly when selectedUser are loaded', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().user.getUserById.mockResolvedValue({ + id: 1, + user_identifier: 'LongerUserName', + user_record_end_date: 'end', + role_names: ['role1', 'role2'] + }); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue({ + project: null + } as any); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }] + } as any); + + const { getAllByTestId } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('user-detail-title').length).toEqual(1); + expect(getAllByTestId('projects_header').length).toEqual(1); + }); + }); +}); diff --git a/app/src/features/admin/users/UsersDetailPage.tsx b/app/src/features/admin/users/UsersDetailPage.tsx new file mode 100644 index 0000000000..78bd8699a7 --- /dev/null +++ b/app/src/features/admin/users/UsersDetailPage.tsx @@ -0,0 +1,60 @@ +import Box from '@material-ui/core/Box'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Container from '@material-ui/core/Container'; +import Grid from '@material-ui/core/Grid'; +import React, { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import { IGetUserResponse } from '../../../interfaces/useUserApi.interface'; +import UsersDetailHeader from './UsersDetailHeader'; +import UsersDetailProjects from './UsersDetailProjects'; + +/** + * Page to display user details. + * + * @return {*} + */ +const UsersDetailPage: React.FC = (props) => { + const urlParams = useParams(); + const biohubApi = useBiohubApi(); + + const [selectedUser, setSelectedUser] = useState(null); + + useEffect(() => { + if (selectedUser) { + return; + } + + const getUser = async () => { + const id = urlParams['id']; + const user = await biohubApi.user.getUserById(Number(id)); + setSelectedUser(user); + }; + + getUser(); + }, [biohubApi.user, urlParams, selectedUser]); + + if (!selectedUser) { + return ; + } + + return ( + <> + + + + + + + + + + + + + + + ); +}; + +export default UsersDetailPage; diff --git a/app/src/features/admin/users/UsersDetailProjects.test.tsx b/app/src/features/admin/users/UsersDetailProjects.test.tsx new file mode 100644 index 0000000000..88a05a39ae --- /dev/null +++ b/app/src/features/admin/users/UsersDetailProjects.test.tsx @@ -0,0 +1,458 @@ +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { DialogContextProvider } from 'contexts/dialogContext'; +import { createMemoryHistory } from 'history'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import React from 'react'; +import { Router } from 'react-router'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; +import UsersDetailProjects from './UsersDetailProjects'; + +const history = createMemoryHistory(); + +jest.mock('../../../hooks/useBioHubApi'); + +const mockUseBiohubApi = { + project: { + getAllUserProjectsForView: jest.fn, []>(), + removeProjectParticipant: jest.fn, []>(), + updateProjectParticipantRole: jest.fn, []>() + }, + codes: { + getAllCodeSets: jest.fn, []>() + } +}; + +const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock).mockReturnValue( + mockUseBiohubApi +); + +const mockUser = { + id: 1, + user_record_end_date: 'ending', + user_identifier: 'testUser', + role_names: ['system'] +}; + +describe('UsersDetailProjects', () => { + beforeEach(() => { + // clear mocks before each test + mockBiohubApi().project.getAllUserProjectsForView.mockClear(); + mockBiohubApi().codes.getAllCodeSets.mockClear(); + }); + + afterEach(() => { + cleanup(); + }); + + it('shows circular spinner when assignedProjects not yet loaded', async () => { + history.push('/admin/users/1'); + + const { getAllByTestId } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('project-loading').length).toEqual(1); + }); + }); + + it('renders empty list correctly when assignedProjects empty and loaded', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue({ + assignedProjects: [] + } as any); + + const { getAllByTestId, getAllByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('projects_header').length).toEqual(1); + expect(getAllByText('Assigned Projects ()').length).toEqual(1); + expect(getAllByText('No Projects').length).toEqual(1); + }); + }); + + it('renders list of a single project correctly when assignedProjects are loaded', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [{ id: 1, name: 'Project Lead' }] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 2, + name: 'projectName', + system_user_id: 1, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + const { getAllByTestId, getAllByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('projects_header').length).toEqual(1); + expect(getAllByText('Assigned Projects (1)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + }); + }); + + it('renders list of a multiple projects correctly when assignedProjects are loaded', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [{ id: 1, name: 'Project Lead' }] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 1, + name: 'projectName', + system_user_id: 2, + project_role_id: 3, + project_participation_id: 4 + }, + { + project_id: 5, + name: 'secondProjectName', + system_user_id: 6, + project_role_id: 7, + project_participation_id: 8 + } + ]); + + const { getAllByTestId, getAllByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByTestId('projects_header').length).toEqual(1); + expect(getAllByText('Assigned Projects (2)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + expect(getAllByText('secondProjectName').length).toEqual(1); + }); + }); + + it('routes to project id details on click', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [{ id: 1, name: 'Project Lead' }] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 1, + name: 'projectName', + system_user_id: 2, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + const { getAllByText, getByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByText('projectName').length).toEqual(1); + }); + + fireEvent.click(getByText('projectName')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/projects/1/details'); + }); + }); + + describe('Are you sure? Dialog', () => { + it('does nothing if the user clicks `No` or away from the dialog', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [{ id: 1, name: 'Project Lead' }] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 1, + name: 'projectName', + system_user_id: 2, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + const { getAllByText, getByTestId, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByText('projectName').length).toEqual(1); + }); + + fireEvent.click(getByTestId('remove-project-participant-button')); + + await waitFor(() => { + expect(getAllByText('Remove User From Project').length).toEqual(1); + }); + + fireEvent.click(getByText('No')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/users/1'); + }); + }); + + it('deletes User from project if the user clicks on `Yes` ', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [{ id: 1, name: 'Project Lead' }] + } as any); + + mockBiohubApi().project.removeProjectParticipant.mockResolvedValue(true); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 1, + name: 'projectName', + system_user_id: 2, + project_role_id: 3, + project_participation_id: 4 + }, + { + project_id: 5, + name: 'secondProjectName', + system_user_id: 6, + project_role_id: 7, + project_participation_id: 8 + } + ]); + + const { getAllByText, getByText, getAllByTestId } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByText('Assigned Projects (2)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + expect(getAllByText('secondProjectName').length).toEqual(1); + }); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 5, + name: 'secondProjectName', + system_user_id: 6, + project_role_id: 7, + project_participation_id: 8 + } + ]); + + fireEvent.click(getAllByTestId('remove-project-participant-button')[0]); + + await waitFor(() => { + expect(getAllByText('Remove User From Project').length).toEqual(1); + }); + + fireEvent.click(getByText('Yes')); + + await waitFor(() => { + expect(getAllByText('Assigned Projects (1)').length).toEqual(1); + expect(getAllByText('secondProjectName').length).toEqual(1); + }); + }); + }); + + describe('Change users Project Role', () => { + it('renders list of roles to change per project', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [ + { id: 1, name: 'Project Lead' }, + { id: 2, name: 'Editor' }, + { id: 3, name: 'Viewer' } + ] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 2, + name: 'projectName', + system_user_id: 1, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + const { getAllByText, getByText } = render( + + + + ); + + await waitFor(() => { + expect(getAllByText('Assigned Projects (1)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + }); + + fireEvent.click(getByText('Viewer')); + + await waitFor(() => { + expect(getAllByText('Project Lead').length).toEqual(1); + expect(getAllByText('Editor').length).toEqual(1); + expect(getAllByText('Viewer').length).toEqual(2); + }); + }); + + it('renders dialog pop on role selection, does nothing if user clicks `Cancel` ', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [ + { id: 1, name: 'Project Lead' }, + { id: 2, name: 'Editor' }, + { id: 3, name: 'Viewer' } + ] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 2, + name: 'projectName', + system_user_id: 1, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + const { getAllByText, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByText('Assigned Projects (1)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + }); + + fireEvent.click(getByText('Viewer')); + + await waitFor(() => { + expect(getAllByText('Project Lead').length).toEqual(1); + expect(getAllByText('Editor').length).toEqual(1); + expect(getAllByText('Viewer').length).toEqual(2); + }); + + fireEvent.click(getByText('Editor')); + + await waitFor(() => { + expect(getAllByText('Change Project Role?').length).toEqual(1); + }); + + fireEvent.click(getByText('Cancel')); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/admin/users/1'); + }); + }); + + it('renders dialog pop on role selection, Changes role on click of `Change Role` ', async () => { + history.push('/admin/users/1'); + + mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ + coordinator_agency: [{ id: 1, name: 'agency 1' }], + project_roles: [ + { id: 1, name: 'Project Lead' }, + { id: 2, name: 'Editor' }, + { id: 3, name: 'Viewer' } + ] + } as any); + + mockBiohubApi().project.getAllUserProjectsForView.mockResolvedValue([ + { + project_id: 2, + name: 'projectName', + system_user_id: 1, + project_role_id: 3, + project_participation_id: 4 + } + ]); + + mockBiohubApi().project.updateProjectParticipantRole.mockResolvedValue(true); + + const { getAllByText, getByText } = render( + + + + + + ); + + await waitFor(() => { + expect(getAllByText('Assigned Projects (1)').length).toEqual(1); + expect(getAllByText('projectName').length).toEqual(1); + }); + + fireEvent.click(getByText('Viewer')); + + await waitFor(() => { + expect(getAllByText('Project Lead').length).toEqual(1); + expect(getAllByText('Editor').length).toEqual(1); + expect(getAllByText('Viewer').length).toEqual(2); + }); + + fireEvent.click(getByText('Editor')); + + await waitFor(() => { + expect(getAllByText('Change Project Role?').length).toEqual(1); + }); + + fireEvent.click(getByText('Change Role')); + + await waitFor(() => { + expect(getAllByText('Editor').length).toEqual(1); + }); + }); + }); +}); diff --git a/app/src/features/admin/users/UsersDetailProjects.tsx b/app/src/features/admin/users/UsersDetailProjects.tsx new file mode 100644 index 0000000000..2b5f91f1ec --- /dev/null +++ b/app/src/features/admin/users/UsersDetailProjects.tsx @@ -0,0 +1,380 @@ +import Box from '@material-ui/core/Box'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Container from '@material-ui/core/Container'; +import IconButton from '@material-ui/core/IconButton'; +import Link from '@material-ui/core/Link'; +import Paper from '@material-ui/core/Paper'; +import { makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import { mdiMenuDown, mdiTrashCanOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { useHistory, useParams } from 'react-router'; +import { IErrorDialogProps } from '../../../components/dialog/ErrorDialog'; +import { IYesNoDialogProps } from '../../../components/dialog/YesNoDialog'; +import { CustomMenuButton } from '../../../components/toolbar/ActionToolbars'; +import { ProjectParticipantsI18N, SystemUserI18N } from '../../../constants/i18n'; +import { DialogContext } from '../../../contexts/dialogContext'; +import { APIError } from '../../../hooks/api/useAxios'; +import { useBiohubApi } from '../../../hooks/useBioHubApi'; +import { CodeSet, IGetAllCodeSetsResponse } from '../../../interfaces/useCodesApi.interface'; +import { IGetUserProjectsListResponse } from '../../../interfaces/useProjectApi.interface'; +import { IGetUserResponse } from '../../../interfaces/useUserApi.interface'; + +const useStyles = makeStyles((theme) => ({ + actionButton: { + minWidth: '6rem', + '& + button': { + marginLeft: '0.5rem' + } + }, + projectMembersToolbar: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2) + }, + projectMembersTable: { + tableLayout: 'fixed', + '& td': { + verticalAlign: 'middle' + } + } +})); + +export interface IProjectDetailsProps { + userDetails: IGetUserResponse; +} + +/** + * Project details content for a project. + * + * @return {*} + */ +const UsersDetailProjects: React.FC = (props) => { + const { userDetails } = props; + const urlParams = useParams(); + const biohubApi = useBiohubApi(); + const dialogContext = useContext(DialogContext); + const history = useHistory(); + const classes = useStyles(); + + const [codes, setCodes] = useState(); + const [isLoadingCodes, setIsLoadingCodes] = useState(true); + const [assignedProjects, setAssignedProjects] = useState(); + + const handleGetUserProjects = useCallback( + async (userId: number) => { + const userProjectsListResponse = await biohubApi.project.getAllUserProjectsForView(userId); + setAssignedProjects(userProjectsListResponse); + }, + [biohubApi.project] + ); + + const refresh = () => handleGetUserProjects(userDetails.id); + + useEffect(() => { + if (assignedProjects) { + return; + } + + handleGetUserProjects(userDetails.id); + }, [userDetails.id, assignedProjects, handleGetUserProjects]); + + useEffect(() => { + const getCodes = async () => { + const codesResponse = await biohubApi.codes.getAllCodeSets(); + + if (!codesResponse) { + return; + } + + setCodes(codesResponse); + }; + + if (isLoadingCodes && !codes) { + getCodes(); + setIsLoadingCodes(false); + } + }, [urlParams, biohubApi.codes, isLoadingCodes, codes]); + + const handleRemoveProjectParticipant = async (projectId: number, projectParticipationId: number) => { + try { + const response = await biohubApi.project.removeProjectParticipant(projectId, projectParticipationId); + + if (!response) { + openErrorDialog({ + dialogTitle: SystemUserI18N.removeUserErrorTitle, + dialogText: SystemUserI18N.removeUserErrorText + }); + return; + } + + dialogContext.setSnackbar({ + open: true, + snackbarMessage: ( + + User {userDetails.user_identifier} removed from project. + + ) + }); + + handleGetUserProjects(userDetails.id); + } catch (error) { + openErrorDialog({ + dialogTitle: SystemUserI18N.removeUserErrorTitle, + dialogText: SystemUserI18N.removeUserErrorText, + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError).errors + }); + } + }; + + const defaultErrorDialogProps: Partial = { + onClose: () => dialogContext.setErrorDialog({ open: false }), + onOk: () => dialogContext.setErrorDialog({ open: false }) + }; + + const defaultYesNoDialogProps: Partial = { + onClose: () => dialogContext.setYesNoDialog({ open: false }), + onNo: () => dialogContext.setYesNoDialog({ open: false }) + }; + + const openYesNoDialog = (yesNoDialogProps?: Partial) => { + dialogContext.setYesNoDialog({ + ...defaultYesNoDialogProps, + ...yesNoDialogProps, + open: true + }); + }; + + const openErrorDialog = useCallback( + (errorDialogProps?: Partial) => { + dialogContext.setErrorDialog({ + ...defaultErrorDialogProps, + ...errorDialogProps, + open: true + }); + }, + [defaultErrorDialogProps, dialogContext] + ); + + if (!codes || !assignedProjects) { + return ; + } + + return ( + + + + + + Assigned Projects ({assignedProjects?.length}) + + + + + + + Project Name + Project Role + + Actions + + + + + {assignedProjects.length > 0 && + assignedProjects?.map((row) => ( + + + history.push(`/admin/projects/${row.project_id}/details`)} + aria-current="page"> + + {row.name} + + + + + + + + + + + + + openYesNoDialog({ + dialogTitle: SystemUserI18N.removeUserFromProject, + dialogContent: ( + <> + + Removing user {userDetails.user_identifier} will revoke their + access to the project. + + + Are you sure you want to proceed? + + + ), + yesButtonProps: { color: 'secondary' }, + onYes: () => { + handleRemoveProjectParticipant(row.project_id, row.project_participation_id); + dialogContext.setYesNoDialog({ open: false }); + } + }) + }> + + + + + + ))} + {!assignedProjects.length && ( + + + + No Projects + + + + )} + +
+
+
+
+
+ ); +}; + +export default UsersDetailProjects; + +export interface IChangeProjectRoleMenuProps { + row: IGetUserProjectsListResponse; + user_identifier: string; + projectRoleCodes: CodeSet; + refresh: () => void; +} + +const ChangeProjectRoleMenu: React.FC = (props) => { + const { row, user_identifier, projectRoleCodes, refresh } = props; + + const dialogContext = useContext(DialogContext); + const biohubApi = useBiohubApi(); + + const errorDialogProps = { + dialogTitle: ProjectParticipantsI18N.updateParticipantRoleErrorTitle, + dialogText: ProjectParticipantsI18N.updateParticipantRoleErrorText, + open: false, + onClose: () => { + dialogContext.setErrorDialog({ open: false }); + }, + onOk: () => { + dialogContext.setErrorDialog({ open: false }); + } + }; + + const displayErrorDialog = (textDialogProps?: Partial) => { + dialogContext.setErrorDialog({ ...errorDialogProps, ...textDialogProps, open: true }); + }; + + const handleChangeUserPermissionsClick = (item: IGetUserProjectsListResponse, newRole: string, newRoleId: number) => { + dialogContext.setYesNoDialog({ + dialogTitle: 'Change Project Role?', + dialogContent: ( + <> + + Change user {user_identifier}'s role to {newRole}? + + + ), + yesButtonLabel: 'Change Role', + noButtonLabel: 'Cancel', + yesButtonProps: { color: 'primary' }, + open: true, + onClose: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onNo: () => { + dialogContext.setYesNoDialog({ open: false }); + }, + onYes: () => { + changeProjectParticipantRole(item, newRole, newRoleId); + dialogContext.setYesNoDialog({ open: false }); + } + }); + }; + + const changeProjectParticipantRole = async ( + item: IGetUserProjectsListResponse, + newRole: string, + newRoleId: number + ) => { + if (!item?.project_participation_id) { + return; + } + + try { + const status = await biohubApi.project.updateProjectParticipantRole( + item.project_id, + item.project_participation_id, + newRoleId + ); + + if (!status) { + displayErrorDialog(); + return; + } + + dialogContext.setSnackbar({ + open: true, + snackbarMessage: ( + + User {user_identifier}'s role changed to {newRole}. + + ) + }); + + refresh(); + } catch (error) { + displayErrorDialog({ + dialogTitle: SystemUserI18N.updateProjectLeadRoleErrorTitle, + dialogText: SystemUserI18N.updateProjectLeadRoleErrorText, + dialogError: (error as APIError).message, + dialogErrorDetails: (error as APIError).errors + }); + } + }; + + const currentProjectRoleName = projectRoleCodes.find((item) => item.id === row.project_role_id)?.name; + + return ( + { + return { + menuLabel: roleCode.name, + menuOnClick: () => handleChangeUserPermissionsClick(row, roleCode.name, roleCode.id) + }; + })} + buttonEndIcon={} + /> + ); +}; diff --git a/app/src/features/observations/components/ObservationSubmissionCSV.tsx b/app/src/features/observations/components/ObservationSubmissionCSV.tsx index f752b37d10..990aaf1138 100644 --- a/app/src/features/observations/components/ObservationSubmissionCSV.tsx +++ b/app/src/features/observations/components/ObservationSubmissionCSV.tsx @@ -17,13 +17,12 @@ import { handleChangePage, handleChangeRowsPerPage } from 'utils/tablePagination const useStyles = makeStyles({ table: { - minWidth: 650 - }, - heading: { - fontWeight: 'bold' - }, - tableCellBorderTop: { - borderTop: '1px solid rgba(224, 224, 224, 1)' + '& th': { + whiteSpace: 'nowrap' + }, + '& td': { + whiteSpace: 'nowrap' + } } }); @@ -99,12 +98,19 @@ const ObservationSubmissionCSV: React.FC = (prop return ( <> - - {submissionCSVDetails.data.map((dataItem: IGetSubmissionCSVForViewItem, dataItemIndex: number) => ( - - ))} - - + + + {submissionCSVDetails.data.map((dataItem: IGetSubmissionCSVForViewItem, dataItemIndex: number) => ( + + ))} + + + {submissionCSVDetails.data.map((dataItem: IGetSubmissionCSVForViewItem, dataItemIndex: number) => ( @@ -112,9 +118,7 @@ const ObservationSubmissionCSV: React.FC = (prop {dataItem.headers.map((header: string, headerIndex: number) => ( - - {header} - + {header} ))} diff --git a/app/src/features/permits/CreatePermitPage.test.tsx b/app/src/features/permits/CreatePermitPage.test.tsx index e1d94bd8ec..9b597b7814 100644 --- a/app/src/features/permits/CreatePermitPage.test.tsx +++ b/app/src/features/permits/CreatePermitPage.test.tsx @@ -1,11 +1,11 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { DialogContextProvider } from 'contexts/dialogContext'; +import { createMemoryHistory } from 'history'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import React from 'react'; -import CreatePermitPage from './CreatePermitPage'; import { MemoryRouter, Router } from 'react-router'; -import { createMemoryHistory } from 'history'; -import { DialogContextProvider } from 'contexts/dialogContext'; +import CreatePermitPage from './CreatePermitPage'; const history = createMemoryHistory(); diff --git a/app/src/features/permits/CreatePermitPage.tsx b/app/src/features/permits/CreatePermitPage.tsx index 61b71339a2..45a04502c4 100644 --- a/app/src/features/permits/CreatePermitPage.tsx +++ b/app/src/features/permits/CreatePermitPage.tsx @@ -7,22 +7,22 @@ import Paper from '@material-ui/core/Paper'; import { Theme } from '@material-ui/core/styles/createMuiTheme'; import makeStyles from '@material-ui/core/styles/makeStyles'; import Typography from '@material-ui/core/Typography'; +import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent'; import { CreatePermitsI18N } from 'constants/i18n'; +import { DialogContext } from 'contexts/dialogContext'; +import ProjectCoordinatorForm from 'features/projects/components/ProjectCoordinatorForm'; +import ProjectPermitForm from 'features/projects/components/ProjectPermitForm'; import { Formik, FormikProps } from 'formik'; +import * as History from 'history'; +import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { ICreatePermitsRequest } from 'interfaces/usePermitApi.interface'; import React, { useContext, useEffect, useRef, useState } from 'react'; import { Prompt, useHistory, useParams } from 'react-router'; -import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent'; -import { DialogContext } from 'contexts/dialogContext'; -import yup from 'utils/YupSchema'; -import ProjectCoordinatorForm from 'features/projects/components/ProjectCoordinatorForm'; -import ProjectPermitForm from 'features/projects/components/ProjectPermitForm'; import { validateFormFieldsAndReportCompletion } from 'utils/customValidation'; -import { APIError } from 'hooks/api/useAxios'; -import * as History from 'history'; -import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; -import { ICreatePermitsRequest } from 'interfaces/usePermitApi.interface'; +import yup from 'utils/YupSchema'; const useStyles = makeStyles((theme: Theme) => ({ actionButton: { diff --git a/app/src/features/permits/PermitsPage.test.tsx b/app/src/features/permits/PermitsPage.test.tsx index a0b7680931..efc6c02c5f 100644 --- a/app/src/features/permits/PermitsPage.test.tsx +++ b/app/src/features/permits/PermitsPage.test.tsx @@ -1,9 +1,9 @@ +import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; import { MemoryRouter, Router } from 'react-router-dom'; -import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import PermitsPage from './PermitsPage'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { createMemoryHistory } from 'history'; const history = createMemoryHistory(); @@ -47,14 +47,14 @@ describe('PermitsPage', () => { id: 1, number: '123', type: 'Wildlife', - coordinator_agency: 'coordinator agency', + coordinator_agency: 'contact agency', project_name: 'Project 1' }, { id: 2, number: '1234', type: 'Wildlife', - coordinator_agency: 'coordinator agency 2', + coordinator_agency: 'contact agency 2', project_name: null } ]); @@ -77,7 +77,7 @@ describe('PermitsPage', () => { id: 1, number: '123', type: 'Wildlife', - coordinator_agency: 'coordinator agency', + coordinator_agency: 'contact agency', project_name: 'Project 1' } ]); diff --git a/app/src/features/permits/PermitsPage.tsx b/app/src/features/permits/PermitsPage.tsx index 5e08d916fd..b4b706b848 100644 --- a/app/src/features/permits/PermitsPage.tsx +++ b/app/src/features/permits/PermitsPage.tsx @@ -2,8 +2,6 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import Container from '@material-ui/core/Container'; import Divider from '@material-ui/core/Divider'; -import { mdiPlus } from '@mdi/js'; -import Icon from '@mdi/react'; import Paper from '@material-ui/core/Paper'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; @@ -12,10 +10,12 @@ import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Typography from '@material-ui/core/Typography'; +import { mdiPlus } from '@mdi/js'; +import Icon from '@mdi/react'; import { useBiohubApi } from 'hooks/useBioHubApi'; +import { IGetPermitsListResponse } from 'interfaces/usePermitApi.interface'; import React, { useEffect, useState } from 'react'; import { useHistory } from 'react-router'; -import { IGetPermitsListResponse } from 'interfaces/usePermitApi.interface'; /** * Page to display a list of permits. @@ -60,7 +60,7 @@ const PermitsPage: React.FC = () => { Number Type - Coordinator Agency + Contact Agency Associated Project @@ -83,7 +83,7 @@ const PermitsPage: React.FC = () => { Number Type - Coordinator Agency + Contact Agency Associated Project diff --git a/app/src/features/permits/PermitsRouter.tsx b/app/src/features/permits/PermitsRouter.tsx index e0236bd4dd..8a387bca50 100644 --- a/app/src/features/permits/PermitsRouter.tsx +++ b/app/src/features/permits/PermitsRouter.tsx @@ -3,32 +3,29 @@ import PermitsLayout from 'features/permits/PermitsLayout'; import React from 'react'; import { Redirect, Switch } from 'react-router'; import AppRoute from 'utils/AppRoute'; -import PrivateRoute from 'utils/PrivateRoute'; import PermitsPage from './PermitsPage'; -interface IPermitsRouterProps { - classes: any; -} - /** - * Router for all `/permit/*` pages. + * Router for all `/admin/permits/*` pages. * * @param {*} props * @return {*} */ -const PermitsRouter: React.FC = (props) => { +const PermitsRouter: React.FC = () => { return ( - - + + + + + + + + {/* Catch any unknown routes, and re-direct to the not found page */} - } /> + + + ); }; diff --git a/app/src/features/permits/__snapshots__/CreatePermitPage.test.tsx.snap b/app/src/features/permits/__snapshots__/CreatePermitPage.test.tsx.snap index 308e30e438..6b82699525 100644 --- a/app/src/features/permits/__snapshots__/CreatePermitPage.test.tsx.snap +++ b/app/src/features/permits/__snapshots__/CreatePermitPage.test.tsx.snap @@ -35,17 +35,13 @@ exports[`CreatePermitPage renders correctly when codes are loaded 1`] = `
-
-

- Coordinator Information -

-
+ Coordinator Information +

+ +
+
+

+ Permits +

+
+ +
+
+
+ + + + + + + + + + + + + +
+ Number + + Type +
+ 123 + + Scientific +
+
+
+
+
+

+ Funding Sources +

+
+ +
+
+
+ + + + + + + + + + + + + + + +
+ Agency + + Amount + + Dates +
+ Funding Agency Blah + + $100 + + Apr 9, 2000 - May 10, 2000 +
+
+
+ +
+
+
+

+ Purpose and Methodology Data +

+
+ +
+
+
+
+
- Survey Methodology + Intended Outcome
- method + Intended Outcome 1
- Permit + Additional Details
- 123 - Scientific + details
- Funding Sources + Field Method
- Funding Agency Blah | $100 | April 09, 2000 - May 10, 2000 + Recruitment
+
-
-
-
-
-
- Purpose -
-
-

- survey purpose -

-
-
-
-
-
-
- -
-
-
-
-

- Study Area -

- -
-
-
+ Season 1 + +
- Survey Area Name + Vantage Code
- study area + Vantage Code 1
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-

Proprietary Data

- + + +
+
- Proprietary Data Category + Proprietor Name
- proprietor type + prop name
- Proprietor Name + Data Category
- prop name + proprietor type
- Data and Information Sharing Agreement Required + DISA Required
-
-
-
- Category Rationale -
-
-

- rationale -

-
+ Category Rationale + +

+ rationale +

diff --git a/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap b/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap index b36c373f75..3581de9db7 100644 --- a/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap +++ b/app/src/features/surveys/view/__snapshots__/SurveyPage.test.tsx.snap @@ -75,13 +75,13 @@ exports[`SurveyPage renders a spinner if no survey is loaded 1`] = ` exports[`SurveyPage renders correctly with no end date 1`] = ` - -
- - - +

+ Funding Sources +

+
+ +
+
+
+ + + + + + + + + + + + + + + +
+ Agency + + Amount + + Dates +
+ Funding Agency Blah + + $100 + + Apr 9, 2000 - May 10, 2000 +
+
+ +
- - Summary Results - +

+ Purpose and Methodology Data +

+
+ +
+
+
+
+
+
+
+ Intended Outcome +
+
+
+
+
+ Additional Details +
+
+ details +
+
+
+
+ Field Method +
+
+
+
+
+
+ Ecological Season +
+
+
+
+
+ Vantage Code +
+
+
+
+
+
+ Surveyed all areas? +
+
+ Yes - all areas were surveyed +
+
+
+
-
- +
- - - +

+ Proprietary Data +

+
+ +
+
+
+
+
+
+
+ Proprietor Name +
+
+ prop name +
+
+
+
+ Data Category +
+
+ proprietor type +
+
+
+
+ DISA Required +
+
+ Yes +
+
+
+
+ Category Rationale +
+

+ rationale +

+
+
+
+
+ +
+ +
+
+ + + +
+
+
+
+
+

+ Documents +

+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + +
+ Name + + Type + + File Size + + Last Modified + + Security + +
+ No Attachments +
+
+
+
+
+
+ +
+
- - Summary Results - +

+ Purpose and Methodology Data +

+
+ +
+
+
+
+
+
+
+ Intended Outcome +
+
+
+
+
+ Additional Details +
+
+ details +
+
+
+
+ Field Method +
+
+
+
+
+
+ Ecological Season +
+
+
+
+
+ Vantage Code +
+
+
+
+
+
+ Surveyed all areas? +
+
+ Yes - all areas were surveyed +
+
+
+
-
- +
- - - +

+ Proprietary Data +

+
+ +
+
+
+
+
+
+
+ Proprietor Name +
+
+ prop name +
+
+
+
+ Data Category +
+
+ proprietor type +
+
+
+
+ DISA Required +
+
+ Yes +
+
+
+
+ Category Rationale +
+

+ rationale +

+
+
+
+
+ +
+ +
+
+ + + +
+
+
+
+
+

+ Documents +

+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + +
+ Name + + Type + + File Size + + Last Modified + + Security + +
+ No Attachments +
+
+
+
+
+
+ +
+
+
+

+ Study Area +

- - + + + + + + + Edit + + + - +
+
+
+
+
+
+
+ +
+
+
+
+
+ + + + + +
+
+
+
+
+
+ +
+
+ +
+

+ Study Area Name +

+

+ study area +

+
+
+
+
+
- - + + +
+
- -
+
diff --git a/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx b/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx index 2f05ac50fe..97fcc4398b 100644 --- a/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx +++ b/app/src/features/surveys/view/components/SurveyGeneralInformation.test.tsx @@ -1,10 +1,10 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; import React from 'react'; import { codes } from 'test-helpers/code-helpers'; -import SurveyGeneralInformation from './SurveyGeneralInformation'; import { getProjectForViewResponse } from 'test-helpers/project-helpers'; +import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import SurveyGeneralInformation from './SurveyGeneralInformation'; jest.mock('../../../../hooks/useBioHubApi'); const mockUseBiohubApi = { @@ -13,6 +13,9 @@ const mockUseBiohubApi = { updateSurvey: jest.fn(), getSurveyPermits: jest.fn(), getSurveyFundingSources: jest.fn() + }, + taxonomy: { + getSpeciesFromIds: jest.fn().mockResolvedValue({ searchResponse: [] }) } }; @@ -40,6 +43,7 @@ describe('SurveyGeneralInformation', () => { mockBiohubApi().survey.updateSurvey.mockClear(); mockBiohubApi().survey.getSurveyPermits.mockClear(); mockBiohubApi().survey.getSurveyFundingSources.mockClear(); + mockBiohubApi().taxonomy.getSpeciesFromIds.mockClear(); }); afterEach(() => { @@ -54,8 +58,8 @@ describe('SurveyGeneralInformation', () => { survey_details: { ...getSurveyForViewResponse.survey_details, end_date: (null as unknown) as string, - focal_species: ['species 1'], - ancillary_species: ['ancillary species'] + focal_species: [1], + ancillary_species: [2] } }} codes={codes} @@ -79,8 +83,10 @@ describe('SurveyGeneralInformation', () => { id: 1, survey_name: 'survey name is this', survey_purpose: 'survey purpose is this', - focal_species: ['species 1'], - ancillary_species: ['ancillary species'], + focal_species: [1], + focal_species_names: ['focal species 1'], + ancillary_species: [2], + ancillary_species_names: ['ancillary species 2'], common_survey_methodology_id: 1, start_date: '1999-09-09', end_date: '2021-01-25', @@ -108,13 +114,13 @@ describe('SurveyGeneralInformation', () => { { pfsId: 1, amount: 100, startDate: '2000-04-09 11:53:53', endDate: '2000-05-10 11:53:53', agencyName: 'agency' } ]); - const { getByText, queryByText } = renderContainer(); + const { getByText, getByTestId, queryByText } = renderContainer(); await waitFor(() => { expect(getByText('General Information')).toBeVisible(); }); - fireEvent.click(getByText('Edit')); + fireEvent.click(getByTestId('edit-general-info')); await waitFor(() => { expect(mockBiohubApi().survey.getSurveyForUpdate).toBeCalledWith(1, getSurveyForViewResponse.survey_details.id, [ @@ -132,7 +138,7 @@ describe('SurveyGeneralInformation', () => { expect(queryByText('Edit Survey General Information')).not.toBeInTheDocument(); }); - fireEvent.click(getByText('Edit')); + fireEvent.click(getByTestId('edit-general-info')); await waitFor(() => { expect(getByText('Edit Survey General Information')).toBeVisible(); @@ -147,8 +153,10 @@ describe('SurveyGeneralInformation', () => { id: 1, survey_name: 'survey name is this', survey_purpose: 'survey purpose is this', - focal_species: ['species 1'], - ancillary_species: ['ancillary species'], + focal_species: [1], + focal_species_names: ['focal species 1'], + ancillary_species: [2], + ancillary_species_names: ['ancillary species 2'], common_survey_methodology_id: 1, start_date: '1999-09-09', end_date: '2021-01-25', @@ -177,13 +185,13 @@ describe('SurveyGeneralInformation', () => { it('displays an error dialog when fetching the update data fails', async () => { mockBiohubApi().survey.getSurveyForUpdate.mockResolvedValue(null); - const { getByText, queryByText } = renderContainer(); + const { getByText, getByTestId, queryByText } = renderContainer(); await waitFor(() => { expect(getByText('General Information')).toBeVisible(); }); - fireEvent.click(getByText('Edit')); + fireEvent.click(getByTestId('edit-general-info')); await waitFor(() => { expect(getByText('Error Editing Survey General Information')).toBeVisible(); @@ -199,13 +207,13 @@ describe('SurveyGeneralInformation', () => { it('shows error dialog with API error message when getting survey data for update fails', async () => { mockBiohubApi().survey.getSurveyForUpdate = jest.fn(() => Promise.reject(new Error('API Error is Here'))); - const { getByText, queryByText, getAllByRole } = renderContainer(); + const { getByText, getByTestId, queryByText, getAllByRole } = renderContainer(); await waitFor(() => { expect(getByText('General Information')).toBeVisible(); }); - fireEvent.click(getByText('Edit')); + fireEvent.click(getByTestId('edit-general-info')); await waitFor(() => { expect(queryByText('API Error is Here')).toBeInTheDocument(); @@ -246,13 +254,13 @@ describe('SurveyGeneralInformation', () => { ]); mockBiohubApi().survey.updateSurvey = jest.fn(() => Promise.reject(new Error('API Error is Here'))); - const { getByText, queryByText } = renderContainer(); + const { getByText, getByTestId, queryByText } = renderContainer(); await waitFor(() => { expect(getByText('General Information')).toBeVisible(); }); - fireEvent.click(getByText('Edit')); + fireEvent.click(getByTestId('edit-general-info')); await waitFor(() => { expect(mockBiohubApi().survey.getSurveyForUpdate).toBeCalledWith(1, getSurveyForViewResponse.survey_details.id, [ diff --git a/app/src/features/surveys/view/components/SurveyGeneralInformation.tsx b/app/src/features/surveys/view/components/SurveyGeneralInformation.tsx index d130c2ef4c..5a977c8702 100644 --- a/app/src/features/surveys/view/components/SurveyGeneralInformation.tsx +++ b/app/src/features/surveys/view/components/SurveyGeneralInformation.tsx @@ -1,33 +1,40 @@ import Box from '@material-ui/core/Box'; -import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; import Grid from '@material-ui/core/Grid'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; import Typography from '@material-ui/core/Typography'; import { mdiPencilOutline } from '@mdi/js'; import Icon from '@mdi/react'; +import EditDialog from 'components/dialog/EditDialog'; +import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { H3ButtonToolbar } from 'components/toolbar/ActionToolbars'; import { DATE_FORMAT, DATE_LIMIT } from 'constants/dateTimeFormats'; +import { EditSurveyGeneralInformationI18N } from 'constants/i18n'; import GeneralInformationForm, { GeneralInformationInitialValues, GeneralInformationYupSchema, IGeneralInformationForm } from 'features/surveys/components/GeneralInformationForm'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import { IGetProjectForViewResponse } from 'interfaces/useProjectApi.interface'; import { - IGetSurveyForViewResponse, IGetSurveyForUpdateResponseDetails, - UPDATE_GET_SURVEY_ENTITIES, - SurveyPermits, + IGetSurveyForViewResponse, + ISurveyFundingSourceForView, SurveyFundingSources, - ISurveyFundingSourceForView + SurveyPermits, + UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; +import moment from 'moment'; import React, { useState } from 'react'; import { getFormattedAmount, getFormattedDate, getFormattedDateRangeString } from 'utils/Utils'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; -import { APIError } from 'hooks/api/useAxios'; -import EditDialog from 'components/dialog/EditDialog'; -import { EditSurveyGeneralInformationI18N } from 'constants/i18n'; -import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; -import moment from 'moment'; import yup from 'utils/YupSchema'; export interface ISurveyGeneralInformationProps { @@ -48,7 +55,6 @@ const SurveyGeneralInformation: React.FC = (prop const { projectForViewData, surveyForViewData: { survey_details }, - codes, refresh } = props; @@ -162,16 +168,6 @@ const SurveyGeneralInformation: React.FC = (prop component={{ element: ( { - return { value: item.id, label: item.name }; - }) || [] - } - common_survey_methodologies={ - codes?.common_survey_methodologies?.map((item) => { - return { value: item.id, label: item.name }; - }) || [] - } permit_numbers={ surveyPermits?.map((item) => { return { value: item.number, label: `${item.number} - ${item.type}` }; @@ -218,7 +214,7 @@ const SurveyGeneralInformation: React.FC = (prop end_date: yup .string() .isValidDateString() - .isEndDateAfterStartDate('start_date') + .isEndDateSameOrAfterStartDate('start_date') .isBeforeDate( projectForViewData.project.end_date, DATE_FORMAT.ShortDateFormat, @@ -239,19 +235,16 @@ const SurveyGeneralInformation: React.FC = (prop /> - - General Information - - + } + buttonOnClick={() => handleDialogEditOpen()} + buttonProps={{ 'data-testid': 'edit-general-info' }} + toolbarProps={{ disableGutters: true }} + /> +
@@ -270,7 +263,7 @@ const SurveyGeneralInformation: React.FC = (prop {survey_details.end_date ? ( <> {getFormattedDateRangeString( - DATE_FORMAT.ShortMediumDateFormat2, + DATE_FORMAT.ShortMediumDateFormat, survey_details.start_date, survey_details.end_date )} @@ -278,7 +271,7 @@ const SurveyGeneralInformation: React.FC = (prop ) : ( <> Start Date:{' '} - {getFormattedDateRangeString(DATE_FORMAT.ShortMediumDateFormat2, survey_details.start_date)} + {getFormattedDateRangeString(DATE_FORMAT.ShortMediumDateFormat, survey_details.start_date)} )} @@ -295,7 +288,7 @@ const SurveyGeneralInformation: React.FC = (prop Focal Species - {survey_details.focal_species.map((focalSpecies: string, index: number) => { + {survey_details.focal_species_names?.map((focalSpecies: string, index: number) => { return ( {focalSpecies} @@ -307,73 +300,96 @@ const SurveyGeneralInformation: React.FC = (prop Anciliary Species - {survey_details.ancillary_species?.map((ancillarySpecies: string, index: number) => { + + {survey_details.ancillary_species_names?.map((ancillarySpecies: string, index: number) => { return ( {ancillarySpecies} ); })} - {survey_details.ancillary_species.length <= 0 && ( + {survey_details.ancillary_species_names?.length <= 0 && ( No Ancilliary Species )} - - - Survey Methodology - - - {survey_details.common_survey_methodology || 'No Survey Methodology'} - - - - - Permit - - - {(survey_details.permit_number && `${survey_details.permit_number} - ${survey_details.permit_type}`) || - 'No Permit'} - - - - - Funding Sources - + +
+
+ + } + buttonOnClick={() => handleDialogEditOpen()} + toolbarProps={{ disableGutters: true }} + /> + + + + + Number + Type + + + + + {survey_details.permit_number} + {survey_details.permit_type} + + + {/* + {(survey_details.permit_number && `${survey_details.permit_number} - ${survey_details.permit_type}`) || 'No Permit'} + */} +
+
+
+ + } + buttonOnClick={() => handleDialogEditOpen()} + toolbarProps={{ disableGutters: true }} + /> + + + + + + Agency + Amount + Dates + + + {(!survey_details.funding_sources || survey_details.funding_sources.length === 0) && ( - - No Funding Sources - + + No Funding Sources + )} {survey_details.funding_sources && survey_details.funding_sources?.map((fundingSource: ISurveyFundingSourceForView, index: number) => { return ( - - {fundingSource.agency_name} | {getFormattedAmount(fundingSource.funding_amount)} |{' '} - {getFormattedDateRangeString( - DATE_FORMAT.ShortMediumDateFormat2, - fundingSource.funding_start_date, - fundingSource.funding_end_date - )} - + + {fundingSource.agency_name} + {getFormattedAmount(fundingSource.funding_amount)} + + {getFormattedDateRangeString( + DATE_FORMAT.ShortMediumDateFormat, + fundingSource.funding_start_date, + fundingSource.funding_end_date + )} + + ); })} - - - - - - - - Purpose - - - {survey_details.survey_purpose} - - - - + +
+
); diff --git a/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx b/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx index 471dcde65a..e5df021b07 100644 --- a/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx +++ b/app/src/features/surveys/view/components/SurveyProprietaryData.test.tsx @@ -1,11 +1,11 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; -import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; import React from 'react'; import { codes } from 'test-helpers/code-helpers'; -import SurveyProprietaryData from './SurveyProprietaryData'; import { getProjectForViewResponse } from 'test-helpers/project-helpers'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; +import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import SurveyProprietaryData from './SurveyProprietaryData'; jest.mock('../../../../hooks/useBioHubApi'); const mockUseBiohubApi = { diff --git a/app/src/features/surveys/view/components/SurveyProprietaryData.tsx b/app/src/features/surveys/view/components/SurveyProprietaryData.tsx index 3a90625be1..b47eb68436 100644 --- a/app/src/features/surveys/view/components/SurveyProprietaryData.tsx +++ b/app/src/features/surveys/view/components/SurveyProprietaryData.tsx @@ -1,11 +1,12 @@ import Box from '@material-ui/core/Box'; -import Button from '@material-ui/core/Button'; +import Divider from '@material-ui/core/Divider'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import { mdiPencilOutline } from '@mdi/js'; import Icon from '@mdi/react'; import EditDialog from 'components/dialog/EditDialog'; import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { H3ButtonToolbar } from 'components/toolbar/ActionToolbars'; import { EditSurveyProprietorI18N } from 'constants/i18n'; import ProprietaryDataForm, { IProprietaryDataForm, @@ -149,7 +150,7 @@ const SurveyProprietaryData: React.FC = (props) => { - return { value: item.id, label: item.name }; + return { value: item.id, label: item.name, is_first_nation: item.is_first_nation }; }) || [] } first_nations={ @@ -167,19 +168,15 @@ const SurveyProprietaryData: React.FC = (props) => /> - - Proprietary Data - - + } + buttonOnClick={() => handleDialogEditOpen()} + toolbarProps={{ disableGutters: true }} + /> +
{!survey_proprietor && ( @@ -192,37 +189,33 @@ const SurveyProprietaryData: React.FC = (props) => - Proprietary Data Category + Proprietor Name - {survey_proprietor.proprietary_data_category_name} + {survey_proprietor.proprietor_name} - Proprietor Name + Data Category - {survey_proprietor.proprietor_name} + {survey_proprietor.proprietary_data_category_name} - Data and Information Sharing Agreement Required + DISA Required {survey_proprietor.data_sharing_agreement_required === 'true' ? 'Yes' : 'No'} - - - - Category Rationale - - - {survey_proprietor.category_rationale} - + + Category Rationale + + {survey_proprietor.category_rationale} )} diff --git a/app/src/features/surveys/view/components/SurveyPurposeAndMethodology.tsx b/app/src/features/surveys/view/components/SurveyPurposeAndMethodology.tsx new file mode 100644 index 0000000000..ecdcf1d3d7 --- /dev/null +++ b/app/src/features/surveys/view/components/SurveyPurposeAndMethodology.tsx @@ -0,0 +1,280 @@ +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import { mdiPencilOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import EditDialog from 'components/dialog/EditDialog'; +import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; +import { H3ButtonToolbar } from 'components/toolbar/ActionToolbars'; +import { EditSurveyPurposeAndMethodologyI18N } from 'constants/i18n'; +import PurposeAndMethodologyForm, { + IPurposeAndMethodologyForm, + PurposeAndMethodologyInitialValues, + PurposeAndMethodologyYupSchema +} from 'features/surveys/components/PurposeAndMethodologyForm'; +import { APIError } from 'hooks/api/useAxios'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { IGetProjectForViewResponse } from 'interfaces/useProjectApi.interface'; +import { + IGetSurveyForUpdateResponsePurposeAndMethodology, + IGetSurveyForViewResponse, + UPDATE_GET_SURVEY_ENTITIES +} from 'interfaces/useSurveyApi.interface'; +import React, { useState } from 'react'; +import { StringBoolean } from 'types/misc'; + +export interface ISurveyPurposeAndMethodologyDataProps { + surveyForViewData: IGetSurveyForViewResponse; + codes: IGetAllCodeSetsResponse; + projectForViewData: IGetProjectForViewResponse; + refresh: () => void; +} + +/** + * Purpose and Methodology data content for a survey. + * + * @return {*} + */ +const SurveyPurposeAndMethodologyData: React.FC = (props) => { + const biohubApi = useBiohubApi(); + + const { + projectForViewData, + surveyForViewData: { survey_details, survey_purpose_and_methodology }, + codes, + refresh + } = props; + + const [openEditDialog, setOpenEditDialog] = useState(false); + const [ + surveyPurposeAndMethodologyForUpdate, + setSurveyPurposeAndMethodologyForUpdate + ] = useState(null); + const [purposeAndMethodologyFormData, setPurposeAndMethodologyFormData] = useState( + PurposeAndMethodologyInitialValues + ); + + const [errorDialogProps, setErrorDialogProps] = useState({ + dialogTitle: EditSurveyPurposeAndMethodologyI18N.editErrorTitle, + dialogText: EditSurveyPurposeAndMethodologyI18N.editErrorText, + open: false, + onClose: () => { + setErrorDialogProps({ ...errorDialogProps, open: false }); + }, + onOk: () => { + setErrorDialogProps({ ...errorDialogProps, open: false }); + } + }); + + const showErrorDialog = (textDialogProps?: Partial) => { + setErrorDialogProps({ ...errorDialogProps, ...textDialogProps, open: true }); + }; + + const handleDialogEditOpen = async () => { + if (!survey_purpose_and_methodology) { + setSurveyPurposeAndMethodologyForUpdate(null); + setPurposeAndMethodologyFormData(PurposeAndMethodologyInitialValues); + setOpenEditDialog(true); + return; + } + + let surveyPurposeAndMethodologyResponseData; + + try { + const response = await biohubApi.survey.getSurveyForUpdate(projectForViewData.id, survey_details?.id, [ + UPDATE_GET_SURVEY_ENTITIES.survey_purpose_and_methodology + ]); + + if (!response) { + showErrorDialog({ open: true }); + return; + } + + surveyPurposeAndMethodologyResponseData = response?.survey_purpose_and_methodology || null; + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, open: true }); + return; + } + + setSurveyPurposeAndMethodologyForUpdate(surveyPurposeAndMethodologyResponseData); + + setPurposeAndMethodologyFormData({ + intended_outcome_id: + surveyPurposeAndMethodologyResponseData?.intended_outcome_id || + PurposeAndMethodologyInitialValues.intended_outcome_id, + additional_details: + surveyPurposeAndMethodologyResponseData?.additional_details || + PurposeAndMethodologyInitialValues.additional_details, + field_method_id: + surveyPurposeAndMethodologyResponseData?.field_method_id || PurposeAndMethodologyInitialValues.field_method_id, + ecological_season_id: + surveyPurposeAndMethodologyResponseData?.ecological_season_id || + PurposeAndMethodologyInitialValues.ecological_season_id, + vantage_code_ids: + surveyPurposeAndMethodologyResponseData?.vantage_code_ids || + PurposeAndMethodologyInitialValues.vantage_code_ids, + surveyed_all_areas: + surveyPurposeAndMethodologyResponseData?.surveyed_all_areas || (('' as unknown) as StringBoolean) + }); + + setOpenEditDialog(true); + }; + + const handleDialogEditSave = async (values: IPurposeAndMethodologyForm) => { + const surveyData = { + survey_purpose_and_methodology: { + ...values, + id: surveyPurposeAndMethodologyForUpdate?.id, + revision_count: surveyPurposeAndMethodologyForUpdate?.revision_count + } + }; + + try { + await biohubApi.survey.updateSurvey(projectForViewData.id, survey_details.id, surveyData); + } catch (error) { + const apiError = error as APIError; + showErrorDialog({ dialogText: apiError.message, dialogErrorDetails: apiError.errors, open: true }); + return; + } finally { + setOpenEditDialog(false); + } + + refresh(); + }; + + return ( + <> + { + return { value: item.id, label: item.name, subText: item.description }; + }) || [] + } + field_methods={ + codes?.field_methods?.map((item) => { + return { value: item.id, label: item.name, subText: item.description }; + }) || [] + } + ecological_seasons={ + codes?.ecological_seasons?.map((item) => { + return { value: item.id, label: item.name, subText: item.description }; + }) || [] + } + vantage_codes={ + codes?.vantage_codes?.map((item) => { + return { value: item.id, label: item.name }; + }) || [] + } + /> + ), + initialValues: purposeAndMethodologyFormData, + validationSchema: PurposeAndMethodologyYupSchema + }} + onCancel={() => setOpenEditDialog(false)} + onSave={handleDialogEditSave} + /> + + + } + buttonOnClick={() => handleDialogEditOpen()} + toolbarProps={{ disableGutters: true }} + /> + +
+ {!survey_purpose_and_methodology && ( + + + + The data captured in this survey does not have the purpose and methodology section. + + + + )} + {survey_purpose_and_methodology && ( + + + + Intended Outcome + + + {survey_purpose_and_methodology.intended_outcome_id && + codes?.intended_outcomes?.find( + (item: any) => item.id === survey_purpose_and_methodology.intended_outcome_id + )?.name} + + + + + Additional Details + + + {survey_purpose_and_methodology.additional_details} + + + + + Field Method + + + + {survey_purpose_and_methodology.field_method_id && + codes?.field_methods?.find( + (item: any) => item.id === survey_purpose_and_methodology.field_method_id + )?.name} + + + + + + Ecological Season + + + {survey_purpose_and_methodology.ecological_season_id && + codes?.ecological_seasons?.find( + (item: any) => item.id === survey_purpose_and_methodology.ecological_season_id + )?.name} + + + + + Vantage Code + + {survey_purpose_and_methodology.vantage_code_ids?.map((vc_id: number, index: number) => { + return ( + + {codes?.vantage_codes?.find((item: any) => item.id === vc_id)?.name} + + ); + })} + + + + Surveyed all areas? + + + {(survey_purpose_and_methodology.surveyed_all_areas === 'true' && 'Yes - all areas were surveyed') || + (survey_purpose_and_methodology.surveyed_all_areas === 'false' && + 'No - only some areas were surveyed')} + + + + )} +
+
+ + ); +}; + +export default SurveyPurposeAndMethodologyData; diff --git a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx index 9b15fa02f6..dc4d1a66de 100644 --- a/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx +++ b/app/src/features/surveys/view/components/SurveyStudyArea.test.tsx @@ -1,10 +1,10 @@ import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'; -import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import { Feature } from 'geojson'; +import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; -import SurveyStudyArea from './SurveyStudyArea'; import { getProjectForViewResponse } from 'test-helpers/project-helpers'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import { Feature } from 'geojson'; +import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import SurveyStudyArea from './SurveyStudyArea'; jest.mock('../../../../hooks/useBioHubApi'); const mockUseBiohubApi = { @@ -33,8 +33,6 @@ const renderContainer = () => { ); }; -jest.spyOn(console, 'debug').mockImplementation(() => {}); - describe('SurveyStudyArea', () => { beforeEach(() => { // clear mocks before each test @@ -145,6 +143,7 @@ describe('SurveyStudyArea', () => { end_date: '2021-01-25', biologist_first_name: 'firstttt', biologist_last_name: 'lastttt', + permit_type: '', survey_area_name: 'study area is this', revision_count: 1, geometry diff --git a/app/src/features/surveys/view/components/SurveyStudyArea.tsx b/app/src/features/surveys/view/components/SurveyStudyArea.tsx index 0359c540c0..6123008316 100644 --- a/app/src/features/surveys/view/components/SurveyStudyArea.tsx +++ b/app/src/features/surveys/view/components/SurveyStudyArea.tsx @@ -1,14 +1,18 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import Paper from '@material-ui/core/Paper'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; -import { mdiPencilOutline } from '@mdi/js'; +import { mdiChevronRight, mdiPencilOutline, mdiRefresh } from '@mdi/js'; import Icon from '@mdi/react'; -import { displayInferredLayersInfo } from 'components/boundary/MapBoundary'; +import FullScreenViewMapDialog from 'components/boundary/FullScreenViewMapDialog'; +import InferredLocationDetails, { IInferredLayers } from 'components/boundary/InferredLocationDetails'; import EditDialog from 'components/dialog/EditDialog'; import { ErrorDialog, IErrorDialogProps } from 'components/dialog/ErrorDialog'; import MapContainer from 'components/map/MapContainer'; import OccurrenceFeatureGroup from 'components/map/OccurrenceFeatureGroup'; +import { H2ButtonToolbar } from 'components/toolbar/ActionToolbars'; import { EditSurveyStudyAreaI18N } from 'constants/i18n'; import StudyAreaForm, { IStudyAreaForm, @@ -24,7 +28,7 @@ import { IUpdateSurveyRequest, UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { calculateUpdatedMapBounds } from 'utils/mapBoundaryUploadHelpers'; export interface ISurveyStudyAreaProps { @@ -33,12 +37,29 @@ export interface ISurveyStudyAreaProps { refresh: () => void; } +const useStyles = makeStyles(() => + createStyles({ + zoomToBoundaryExtentBtn: { + padding: '3px', + borderRadius: '4px', + background: '#ffffff', + color: '#000000', + border: '2px solid rgba(0,0,0,0.2)', + backgroundClip: 'padding-box', + '&:hover': { + backgroundColor: '#eeeeee' + } + } + }) +); + /** * Study area content for a survey. * * @return {*} */ const SurveyStudyArea: React.FC = (props) => { + const classes = useStyles(); const biohubApi = useBiohubApi(); const { @@ -47,12 +68,12 @@ const SurveyStudyArea: React.FC = (props) => { refresh } = props; - const surveyGeometry = survey_details?.geometry; + const surveyGeometry = survey_details?.geometry || []; const [openEditDialog, setOpenEditDialog] = useState(false); const [surveyDetailsDataForUpdate, setSurveyDetailsDataForUpdate] = useState(null as any); const [studyAreaFormData, setStudyAreaFormData] = useState(StudyAreaInitialValues); - const [inferredLayersInfo, setInferredLayersInfo] = useState({ + const [inferredLayersInfo, setInferredLayersInfo] = useState({ parks: [], nrm: [], env: [], @@ -60,18 +81,21 @@ const SurveyStudyArea: React.FC = (props) => { }); const [bounds, setBounds] = useState([]); const [nonEditableGeometries, setNonEditableGeometries] = useState([]); + const [showFullScreenViewMapDialog, setShowFullScreenViewMapDialog] = useState(false); + + const zoomToBoundaryExtent = useCallback(() => { + setBounds(calculateUpdatedMapBounds(surveyGeometry)); + }, [surveyGeometry]); useEffect(() => { const nonEditableGeometriesResult = surveyGeometry.map((geom: Feature) => { return { feature: geom }; }); - if (!survey_details.occurrence_submission_id) { - setBounds(calculateUpdatedMapBounds(surveyGeometry)); - } + zoomToBoundaryExtent(); setNonEditableGeometries(nonEditableGeometriesResult); - }, [surveyGeometry, survey_details.occurrence_submission_id]); + }, [surveyGeometry, survey_details.occurrence_submission_id, zoomToBoundaryExtent]); const [errorDialogProps, setErrorDialogProps] = useState({ dialogTitle: EditSurveyStudyAreaI18N.editErrorTitle, @@ -126,6 +150,7 @@ const SurveyStudyArea: React.FC = (props) => { const surveyData = { survey_details: { ...surveyDetailsDataForUpdate.survey_details, + permit_type: '', // TODO 20211108: currently permit insert vs update is dictated by permit_type (needs fixing/updating) survey_area_name: values.survey_area_name, geometry: values.geometry } @@ -144,6 +169,14 @@ const SurveyStudyArea: React.FC = (props) => { refresh(); }; + const handleDialogViewOpen = () => { + setShowFullScreenViewMapDialog(true); + }; + + const handleClose = () => { + setShowFullScreenViewMapDialog(false); + }; + return ( <> = (props) => { onCancel={() => setOpenEditDialog(false)} onSave={handleDialogEditSave} /> + + + } + description={survey_details.survey_area_name} + layers={} + backButtonTitle={'Back To Survey'} + mapTitle={'Study Area'} + /> - - - Study Area - - -
- - - - Survey Area Name - - - {survey_details.survey_area_name} - - - -
- + + } + buttonOnClick={() => handleDialogEditOpen()} + buttonProps={{ variant: 'text' }} + toolbarProps={{ disableGutters: true }} + /> + + = (props) => { setInferredLayersInfo={setInferredLayersInfo} additionalLayers={ survey_details.occurrence_submission_id - ? [] + ? [ + + ] : undefined } /> + {surveyGeometry.length > 0 && ( + + zoomToBoundaryExtent()}> + + + + )} + + + + Study Area Name + + {survey_details.survey_area_name} + + + + + + - - - {displayInferredLayersInfo(inferredLayersInfo.nrm, 'NRM Regions')} - - - {displayInferredLayersInfo(inferredLayersInfo.env, 'ENV Regions')} - - - {displayInferredLayersInfo(inferredLayersInfo.wmu, 'WMU ID/GMZ ID/GMZ Name')} - - - {displayInferredLayersInfo(inferredLayersInfo.parks, 'Parks and EcoReserves')} - - ); diff --git a/app/src/features/surveys/view/components/__snapshots__/SurveyGeneralInformation.test.tsx.snap b/app/src/features/surveys/view/components/__snapshots__/SurveyGeneralInformation.test.tsx.snap index 6c982172ea..4cf624a1aa 100644 --- a/app/src/features/surveys/view/components/__snapshots__/SurveyGeneralInformation.test.tsx.snap +++ b/app/src/features/surveys/view/components/__snapshots__/SurveyGeneralInformation.test.tsx.snap @@ -3,47 +3,59 @@ exports[`SurveyGeneralInformation renders correctly with all fields 1`] = `

General Information

- + + +
+
- October 10, 1998 - February 26, 2021 + Oct 10, 1998 - Feb 26, 2021
- ancillary species 1 + ancillary species 2
-
+
+
+
+
+

+ Permits +

+
+
-
+ + + + + + Edit + + + + +
+
+
+ + -
- Permit -
-
- 123 - Scientific -
- -
+ Number + +
+ + + -
- Funding Sources -
-
- Funding Agency Blah | $100 | April 09, 2000 - May 10, 2000 -
- - + + + + +
+ Type +
+ 123 + + Scientific +
+
+
+
+
+

+ Funding Sources +

-
-
-
- -
-

- survey purpose -

-
-
+ + + + + Edit + + + +
- +
+
+ + + + + + + + + + + + + + + +
+ Agency + + Amount + + Dates +
+ Funding Agency Blah + + $100 + + Apr 9, 2000 - May 10, 2000 +
+
`; @@ -198,44 +349,56 @@ exports[`SurveyGeneralInformation renders correctly with no end date (only start class="MuiBox-root MuiBox-root-1" >

General Information

- + + +
+
Start Date: - October 10, 1998 + Oct 10, 1998
- species 1 + focal species 1
- ancillary species + ancillary species 2
-
+
+
+
+
+

+ Permits +

+
+
-
+ + + + + + Edit + + + + +
+
+
+ + -
- Permit -
-
- 123 - Scientific -
- -
+ Number + +
+ + + -
- Funding Sources -
-
- Funding Agency Blah | $100 | April 09, 2000 - May 10, 2000 -
- - + + + + +
+ Type +
+ 123 + + Scientific +
+
+
+
+
+

+ Funding Sources +

-
-
-
- -
-

- survey purpose -

-
-
+ + + + + Edit + + + +
- +
+
+ + + + + + + + + + + + + + + +
+ Agency + + Amount + + Dates +
+ Funding Agency Blah + + $100 + + Apr 9, 2000 - May 10, 2000 +
+
`; diff --git a/app/src/features/surveys/view/components/__snapshots__/SurveyProprietaryData.test.tsx.snap b/app/src/features/surveys/view/components/__snapshots__/SurveyProprietaryData.test.tsx.snap index c388075198..3eb07b90ea 100644 --- a/app/src/features/surveys/view/components/__snapshots__/SurveyProprietaryData.test.tsx.snap +++ b/app/src/features/surveys/view/components/__snapshots__/SurveyProprietaryData.test.tsx.snap @@ -6,44 +6,56 @@ exports[`SurveyProprietaryData renders correctly 1`] = ` class="MuiBox-root MuiBox-root-1" >

Proprietary Data

- + + +
+
- Proprietary Data Category + Proprietor Name
- proprietor type + prop name
- Proprietor Name + Data Category
- prop name + proprietor type
- Data and Information Sharing Agreement Required + DISA Required
-
-
-
- Category Rationale -
-
-

- rationale -

-
+ Category Rationale + +

+ rationale +

diff --git a/app/src/features/surveys/view/components/__snapshots__/SurveyStudyArea.test.tsx.snap b/app/src/features/surveys/view/components/__snapshots__/SurveyStudyArea.test.tsx.snap index de11b10cf8..ed4af8858e 100644 --- a/app/src/features/surveys/view/components/__snapshots__/SurveyStudyArea.test.tsx.snap +++ b/app/src/features/surveys/view/components/__snapshots__/SurveyStudyArea.test.tsx.snap @@ -3,69 +3,58 @@ exports[`SurveyStudyArea renders correctly 1`] = `
-

Study Area -

- -
-
-
-
-
- Survey Area Name -
-
- study area -
-
+ +
-
+
@@ -223,7 +212,7 @@ exports[`SurveyStudyArea renders correctly 1`] = `
@@ -254,22 +243,89 @@ exports[`SurveyStudyArea renders correctly 1`] = `
+
+ +
-
-
-
-
+

+ Study Area Name +

+

+ study area +

+
+
+
+
+
diff --git a/app/src/hooks/api/useAdminApi.test.ts b/app/src/hooks/api/useAdminApi.test.ts index 523c14db8a..c9dc316257 100644 --- a/app/src/hooks/api/useAdminApi.test.ts +++ b/app/src/hooks/api/useAdminApi.test.ts @@ -13,7 +13,15 @@ describe('useAdminApi', () => { mock.restore(); }); - it('getAccessRequests works as expected', async () => { + it('sendGCNotification works as expected', async () => { + mock.onPost('/api/gcnotify/send').reply(200); + + const result = await useAdminApi(axios).sendGCNotification({ emailAddress: 'test@@email.com' }, { body: 'test' }); + + expect(result).toEqual(true); + }); + + it('getAdministrativeActivities works as expected', async () => { const response = [ { id: 1, @@ -30,27 +38,11 @@ describe('useAdminApi', () => { mock.onGet(`/api/administrative-activities`).reply(200, response); - const result = await useAdminApi(axios).getAccessRequests(); + const result = await useAdminApi(axios).getAdministrativeActivities(); expect(result).toEqual(response); }); - it('updateAccessRequest works as expected', async () => { - mock.onPut(`/api/access-request`).reply(200, true); - - const result = await useAdminApi(axios).updateAccessRequest('userIdentifier', 'identitySource', 2, 2, [1, 2, 3]); - - expect(result).toEqual(true); - }); - - it('updateAdministrativeActivity works as expected', async () => { - mock.onPut(`/api/administrative-activity`).reply(200, true); - - const result = await useAdminApi(axios).updateAdministrativeActivity(2, 2); - - expect(result).toEqual(true); - }); - it('createAdministrativeActivity works as expected', async () => { mock.onPost('/api/administrative-activity').reply(200, { id: 2, @@ -74,22 +66,18 @@ describe('useAdminApi', () => { }); it('addSystemUserRoles works as expected', async () => { - const userId = 1; + mock.onPost(`/api/user/1/system-roles/create`).reply(200, true); - mock.onPost(`/api/user/${userId}/system-roles`).reply(200, 3); + const result = await useAdminApi(axios).addSystemUserRoles(1, [2]); - const result = await useAdminApi(axios).addSystemUserRoles(1, [1, 2, 3]); - - expect(result).toEqual(3); + expect(result).toEqual(true); }); - it('removeSystemUserRoles works as expected', async () => { - const userId = 1; + it('addSystemUser works as expected', async () => { + mock.onPost(`/api/user/add`).reply(200, true); - mock.onDelete(`/api/user/${userId}/system-roles`).reply(200, 3); + const result = await useAdminApi(axios).addSystemUser('userIdentifier', 'identitySource', 1); - const result = await useAdminApi(axios).removeSystemUserRoles(1, [1, 2, 3]); - - expect(result).toEqual(3); + expect(result).toEqual(true); }); }); diff --git a/app/src/hooks/api/useAdminApi.ts b/app/src/hooks/api/useAdminApi.ts index 9d0a342958..fd5b1592c8 100644 --- a/app/src/hooks/api/useAdminApi.ts +++ b/app/src/hooks/api/useAdminApi.ts @@ -1,6 +1,11 @@ import { AxiosInstance } from 'axios'; -import { AdministrativeActivityType, AdministrativeActivityStatusType } from 'constants/misc'; -import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; +import { AdministrativeActivityStatusType, AdministrativeActivityType } from 'constants/misc'; +import { + IAccessRequestDataObject, + IgcNotifyGenericMessage, + IgcNotifyRecipient, + IGetAccessRequestsListResponse +} from 'interfaces/useAdminApi.interface'; import qs from 'qs'; /** @@ -10,17 +15,38 @@ import qs from 'qs'; * @return {*} object whose properties are supported api methods. */ const useAdminApi = (axios: AxiosInstance) => { + /** + * Send notification to recipient + * + * @param {IgcNotifyRecipient} recipient + * @param {IgcNotifyGenericMessage} message + * @return {*} {Promise} + */ + const sendGCNotification = async ( + recipient: IgcNotifyRecipient, + message: IgcNotifyGenericMessage + ): Promise => { + const { status } = await axios.post(`/api/gcnotify/send`, { + recipient, + message + }); + + return status === 200; + }; + /** * Get user access requests * + * @param {AdministrativeActivityType[]} [type=[]] * @param {AdministrativeActivityStatusType[]} [status=[]] - * @returns {*} {Promise} + * @return {*} {Promise} */ - const getAccessRequests = async ( + const getAdministrativeActivities = async ( + type: AdministrativeActivityType[] = [], status: AdministrativeActivityStatusType[] = [] ): Promise => { const { data } = await axios.get(`/api/administrative-activities`, { - params: { type: AdministrativeActivityType.SYSTEM_ACCESS, status }, + params: { type, status }, paramsSerializer: (params) => { return qs.stringify(params); } @@ -29,48 +55,23 @@ const useAdminApi = (axios: AxiosInstance) => { return data; }; - /** - * Update a user access request - * - * @param {string} userIdentifier - * @param {string} identitySource - * @param {number} requestId - * @param {string} requestStatusTypeId - * @param {number[]} [roleIds=[]] - * @returns {*} {Promise} - */ - const updateAccessRequest = async ( + const approveAccessRequest = async ( + administrativeActivityId: number, userIdentifier: string, identitySource: string, - requestId: number, - requestStatusTypeId: number, roleIds: number[] = [] ): Promise => { - const { data } = await axios.put(`/api/access-request`, { + const { data } = await axios.put(`/api/administrative-activity/system-access/${administrativeActivityId}/approve`, { userIdentifier, identitySource, - requestId, - requestStatusTypeId, roleIds: roleIds }); return data; }; - /** - * Update an administrative activity - * - * @param {AxiosInstance} axios - * @returns {*} {Promise} - */ - const updateAdministrativeActivity = async ( - administrativeActivityId: number, - administrativeActivityStatusTypeId: number - ): Promise => { - const { data } = await axios.put(`/api/administrative-activity`, { - id: administrativeActivityId, - status: administrativeActivityStatusTypeId - }); + const denyAccessRequest = async (administrativeActivityId: number): Promise => { + const { data } = await axios.put(`/api/administrative-activity/system-access/${administrativeActivityId}/reject`); return data; }; @@ -78,11 +79,11 @@ const useAdminApi = (axios: AxiosInstance) => { /** * Create a new access request record. * - * @param {unknown} administrativeActivityData + * @param {IAccessRequestDataObject} administrativeActivityData * @return {*} {Promise} */ const createAdministrativeActivity = async ( - administrativeActivityData: unknown + administrativeActivityData: IAccessRequestDataObject ): Promise => { const { data } = await axios.post('/api/administrative-activity', administrativeActivityData); @@ -108,37 +109,40 @@ const useAdminApi = (axios: AxiosInstance) => { * @return {*} {Promise} */ const addSystemUserRoles = async (userId: number, roleIds: number[]): Promise => { - const { data } = await axios.post(`/api/user/${userId}/system-roles`, { roles: roleIds }); + const { data } = await axios.post(`/api/user/${userId}/system-roles/create`, { roles: roleIds }); return data; }; /** - * Remove one or more system roles from a user. + * Adds a new system user with role. * - * @param {number} userId - * @param {number[]} roleIds - * @return {*} {Promise} + * Note: Will fail if the system user already exists. + * + * @param {string} userIdentifier + * @param {string} identitySource + * @param {number} roleId + * @return {*} */ - const removeSystemUserRoles = async (userId: number, roleIds: number[]): Promise => { - const { data } = await axios.delete(`/api/user/${userId}/system-roles`, { - params: { roleId: roleIds }, - paramsSerializer: (params) => { - return qs.stringify(params); - } + const addSystemUser = async (userIdentifier: string, identitySource: string, roleId: number): Promise => { + const { status } = await axios.post(`/api/user/add`, { + identitySource: identitySource, + userIdentifier: userIdentifier, + roleId: roleId }); - return data; + return status === 200; }; return { - getAccessRequests, - updateAccessRequest, - updateAdministrativeActivity, + sendGCNotification, + getAdministrativeActivities, + approveAccessRequest, + denyAccessRequest, createAdministrativeActivity, hasPendingAdministrativeActivities, addSystemUserRoles, - removeSystemUserRoles + addSystemUser }; }; diff --git a/app/src/hooks/api/useAxios.test.ts b/app/src/hooks/api/useAxios.test.ts index 74bdc0fe5e..8da0713944 100644 --- a/app/src/hooks/api/useAxios.test.ts +++ b/app/src/hooks/api/useAxios.test.ts @@ -1,7 +1,7 @@ import { AxiosError } from 'axios'; import { APIError } from './useAxios'; -describe('useAxios', () => { +describe('APIError', () => { it('assigns all values correctly', () => { const error = { name: 'error name', @@ -10,9 +10,6 @@ describe('useAxios', () => { config: { baseURL: 'localhost', url: '/test' - }, - request: { - responseURL: 'localhost/test-error' } } as Partial; @@ -25,6 +22,5 @@ describe('useAxios', () => { expect(apiError.status).toEqual(500); expect(apiError.errors).toEqual(['some errors']); expect(apiError.requestURL).toEqual('localhost/test'); - expect(apiError.responseURL).toEqual('localhost/test-error'); }); }); diff --git a/app/src/hooks/api/useAxios.ts b/app/src/hooks/api/useAxios.ts index 71c22ed477..b64dce9c22 100644 --- a/app/src/hooks/api/useAxios.ts +++ b/app/src/hooks/api/useAxios.ts @@ -3,22 +3,19 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; import { useMemo } from 'react'; import { ensureProtocol } from 'utils/Utils'; -export class APIError { - name: string; +export class APIError extends Error { status: number; - message: string; errors?: (string | object)[]; requestURL?: string; - responseURL?: string; constructor(error: AxiosError) { - this.name = error?.response?.data?.name || error.name; - this.status = error?.response?.data?.status || error?.response?.status || Number(error?.code); - this.message = error?.response?.data?.message || error.message; - this.errors = error?.response?.data?.errors || []; + super(error.response?.data?.message || error.message); + + this.name = error.response?.data?.name || error.name; + this.status = error.response?.data?.status || error.response?.status; + this.errors = error.response?.data?.errors || []; this.requestURL = `${error?.config?.baseURL}${error?.config?.url}`; - this.responseURL = error?.request?.responseURL; } } diff --git a/app/src/hooks/api/useN8NApi.test.ts b/app/src/hooks/api/useN8NApi.test.ts new file mode 100644 index 0000000000..135009c187 --- /dev/null +++ b/app/src/hooks/api/useN8NApi.test.ts @@ -0,0 +1,51 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import useN8NApi from './useN8NApi'; + +describe('useN8NApi', () => { + let mock: any; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + const projectId = 1; + const submissionId = 2; + const fileType = 'csv'; + + it('initiateSubmissionValidation works as expected', async () => { + mock.onPost('/webhook/validate').reply(200); + + const result = await useN8NApi(axios).initiateSubmissionValidation(projectId, submissionId, fileType); + + expect(result).toEqual(undefined); + }); + + it('initiateTransformTemplate works as expected', async () => { + mock.onPost('/webhook/transform').reply(200); + + const result = await useN8NApi(axios).initiateTransformTemplate(projectId, submissionId); + + expect(result).toEqual(undefined); + }); + + it('initiateScrapeOccurrences works as expected', async () => { + mock.onPost('/webhook/scrape').reply(200); + + const result = await useN8NApi(axios).initiateScrapeOccurrences(projectId, submissionId); + + expect(result).toEqual(undefined); + }); + + it('initiateOccurrenceSubmissionProcessing works as expected', async () => { + mock.onPost('/webhook/process-occurrence-submission').reply(200); + + const result = await useN8NApi(axios).initiateOccurrenceSubmissionProcessing(projectId, submissionId, fileType); + + expect(result).toEqual(undefined); + }); +}); diff --git a/app/src/hooks/api/useN8NApi.ts b/app/src/hooks/api/useN8NApi.ts index e9b00dedb8..120f17a59e 100644 --- a/app/src/hooks/api/useN8NApi.ts +++ b/app/src/hooks/api/useN8NApi.ts @@ -7,9 +7,16 @@ import { AxiosInstance } from 'axios'; * @return {*} object whose properties are supported api methods. */ const useN8NApi = (axios: AxiosInstance) => { - // Initiate the validation process for the submitted observations using n8n webhook - const initiateSubmissionValidation = async (submissionId: number, fileType: string) => { + /** + * Initiate the validation process for the submitted observations using n8n webhook + * + * @param {number} projectId + * @param {number} submissionId + * @param {string} fileType + */ + const initiateSubmissionValidation = async (projectId: number, submissionId: number, fileType: string) => { await axios.post('/webhook/validate', { + project_id: projectId, occurrence_submission_id: submissionId, file_type: fileType }); @@ -18,17 +25,25 @@ const useN8NApi = (axios: AxiosInstance) => { /** * Initiate the transformation process for the submitted observation template using n8n. * + * @param {number} projectId * @param {number} submissionId */ - const initiateTransformTemplate = async (submissionId: number) => { + const initiateTransformTemplate = async (projectId: number, submissionId: number) => { await axios.post('/webhook/transform', { + project_id: projectId, occurrence_submission_id: submissionId }); }; - // Initiate the scraping process for the submitted occurrence using n8n webhook - const initiateScrapeOccurrences = async (submissionId: number) => { + /** + * Initiate the scraping process for the submitted occurrence using n8n webhook + * + * @param {number} projectId + * @param {number} submissionId + */ + const initiateScrapeOccurrences = async (projectId: number, submissionId: number) => { await axios.post('/webhook/scrape', { + project_id: projectId, occurrence_submission_id: submissionId }); }; @@ -36,11 +51,13 @@ const useN8NApi = (axios: AxiosInstance) => { /** * Initiate the validation, transformation, and scraping processes for the submitted observation file using n8n. * + * @param {number} projectId * @param {number} submissionId * @param {string} fileType */ - const initiateOccurrenceSubmissionProcessing = async (submissionId: number, fileType: string) => { + const initiateOccurrenceSubmissionProcessing = async (projectId: number, submissionId: number, fileType: string) => { await axios.post('/webhook/process-occurrence-submission', { + project_id: projectId, occurrence_submission_id: submissionId, file_type: fileType }); diff --git a/app/src/hooks/api/useObservationApi.test.ts b/app/src/hooks/api/useObservationApi.test.ts index 549cacd97a..591d303f25 100644 --- a/app/src/hooks/api/useObservationApi.test.ts +++ b/app/src/hooks/api/useObservationApi.test.ts @@ -19,20 +19,21 @@ describe('useObservationApi', () => { it('getObservationSubmission works as expected', async () => { mock .onGet(`/api/project/${projectId}/survey/${surveyId}/observation/submission/get`) - .reply(200, { id: 1, fileName: 'file.txt' }); + .reply(200, { id: 1, inputFileName: 'file.txt' }); const result = await useObservationApi(axios).getObservationSubmission(projectId, surveyId); expect(result.id).toEqual(1); - expect(result.fileName).toEqual('file.txt'); + expect(result.inputFileName).toEqual('file.txt'); }); it('initiateScrapeOccurrences works as expected', async () => { + const projectId = 1; const submissionId = 1; mock.onPost(`/api/dwc/scrape-occurrences`).reply(200, true); - const result = await useObservationApi(axios).initiateScrapeOccurrences(submissionId); + const result = await useObservationApi(axios).initiateScrapeOccurrences(projectId, submissionId); expect(result).toEqual(true); }); @@ -92,28 +93,31 @@ describe('useObservationApi', () => { }); it('initiateXLSXSubmissionTransform works as expected', async () => { + const projectId = 1; const submissionId = 2; mock.onPost(`/api/xlsx/transform`).reply(200, true); - const result = await useObservationApi(axios).initiateXLSXSubmissionTransform(submissionId); + const result = await useObservationApi(axios).initiateXLSXSubmissionTransform(projectId, submissionId); expect(result).toEqual(true); }); it('initiateXLSXSubmissionValidation works as expected', async () => { + const projectId = 1; const submissionId = 2; mock.onPost(`/api/xlsx/validate`).reply(200, true); - const result = await useObservationApi(axios).initiateXLSXSubmissionValidation(submissionId); + const result = await useObservationApi(axios).initiateXLSXSubmissionValidation(projectId, submissionId); expect(result).toEqual(true); }); it('initiateDwCSubmissionValidation works as expected', async () => { + const projectId = 1; const submissionId = 2; mock.onPost(`/api/dwc/validate`).reply(200, true); - const result = await useObservationApi(axios).initiateDwCSubmissionValidation(submissionId); + const result = await useObservationApi(axios).initiateDwCSubmissionValidation(projectId, submissionId); expect(result).toEqual(true); }); @@ -141,6 +145,7 @@ describe('useObservationApi', () => { }); it('getOccurrencesForView works as expected', async () => { + const projectId = 1; const submissionId = 2; const data = { geometry: null, @@ -156,7 +161,7 @@ describe('useObservationApi', () => { mock.onPost(`/api/dwc/view-occurrences`).reply(200, data); - const result = await useObservationApi(axios).getOccurrencesForView(submissionId); + const result = await useObservationApi(axios).getOccurrencesForView(projectId, submissionId); expect(result).toEqual(data); }); diff --git a/app/src/hooks/api/useObservationApi.ts b/app/src/hooks/api/useObservationApi.ts index 6274eb7a7d..c3212bbdd3 100644 --- a/app/src/hooks/api/useObservationApi.ts +++ b/app/src/hooks/api/useObservationApi.ts @@ -1,9 +1,9 @@ import { AxiosInstance, CancelTokenSource } from 'axios'; import { - IGetSubmissionCSVForViewResponse, IGetObservationSubmissionResponse, - IUploadObservationSubmissionResponse, - IGetOccurrencesForViewResponseDetails + IGetOccurrencesForViewResponseDetails, + IGetSubmissionCSVForViewResponse, + IUploadObservationSubmissionResponse } from 'interfaces/useObservationApi.interface'; /** @@ -84,13 +84,16 @@ const useObservationApi = (axios: AxiosInstance) => { /** * Get occurrence information for view-only purposes based on occurrence submission id * + * @param {number} projectId * @param {number} occurrenceSubmissionId * @returns {*} {Promise} */ const getOccurrencesForView = async ( + projectId: number, occurrenceSubmissionId: number ): Promise => { const { data } = await axios.post(`/api/dwc/view-occurrences`, { + project_id: projectId, occurrence_submission_id: occurrenceSubmissionId }); @@ -119,10 +122,13 @@ const useObservationApi = (axios: AxiosInstance) => { /** * Initiate the validation process for the submitted DWC observations + * + * @param {number} projectId * @param {number} submissionId */ - const initiateDwCSubmissionValidation = async (submissionId: number) => { + const initiateDwCSubmissionValidation = async (projectId: number, submissionId: number) => { const { data } = await axios.post(`/api/dwc/validate`, { + project_id: projectId, occurrence_submission_id: submissionId }); @@ -131,10 +137,13 @@ const useObservationApi = (axios: AxiosInstance) => { /** * Initiate the validation process for the submitted XLSX observations + * + * @param {number} projectId * @param {number} submissionId */ - const initiateXLSXSubmissionValidation = async (submissionId: number) => { + const initiateXLSXSubmissionValidation = async (projectId: number, submissionId: number) => { const { data } = await axios.post(`/api/xlsx/validate`, { + project_id: projectId, occurrence_submission_id: submissionId }); @@ -144,10 +153,12 @@ const useObservationApi = (axios: AxiosInstance) => { /** * Initiate the transformation process for the submitted observation template. * + * @param {number} projectId * @param {number} submissionId */ - const initiateXLSXSubmissionTransform = async (submissionId: number) => { + const initiateXLSXSubmissionTransform = async (projectId: number, submissionId: number) => { const { data } = await axios.post(`/api/xlsx/transform`, { + project_id: projectId, occurrence_submission_id: submissionId }); @@ -156,10 +167,13 @@ const useObservationApi = (axios: AxiosInstance) => { /** * Initiate the scraping process for the submitted DWC observations + * + * @param {number} projectId * @param {number} submissionId */ - const initiateScrapeOccurrences = async (submissionId: number) => { + const initiateScrapeOccurrences = async (projectId: number, submissionId: number) => { const { data } = await axios.post(`/api/dwc/scrape-occurrences`, { + project_id: projectId, occurrence_submission_id: submissionId }); diff --git a/app/src/hooks/api/useProjectApi.test.ts b/app/src/hooks/api/useProjectApi.test.ts index 55121fa482..3941bab054 100644 --- a/app/src/hooks/api/useProjectApi.test.ts +++ b/app/src/hooks/api/useProjectApi.test.ts @@ -1,5 +1,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { IEditReportMetaForm } from 'components/attachments/EditReportMetaForm'; +import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; import { IProjectCoordinatorForm } from 'features/projects/components/ProjectCoordinatorForm'; import { IProjectDetailsForm } from 'features/projects/components/ProjectDetailsForm'; import { IProjectFundingForm } from 'features/projects/components/ProjectFundingForm'; @@ -23,9 +25,49 @@ describe('useProjectApi', () => { mock.restore(); }); + const userId = 123; const projectId = 1; const attachmentId = 1; const attachmentType = 'type'; + const attachmentMeta: IReportMetaForm = { + title: 'upload file', + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'file abstract', + year_published: 2000, + attachmentFile: new File(['foo'], 'foo.txt', { + type: 'text/plain' + }) + }; + + const attachmentMetaForUpdate: IEditReportMetaForm = { + title: 'upload file', + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'file abstract', + year_published: 2000, + revision_count: 1 + }; + + it('getAllUserProjectsForView works as expected', async () => { + mock.onGet(`/api/user/${userId}/projects/get`).reply(200, [ + { + project_id: 321, + name: 'test', + system_user_id: 1, + project_role_id: 2, + project_participation_id: 3 + } + ]); + + const result = await useProjectApi(axios).getAllUserProjectsForView(123); + + expect(result[0]).toEqual({ + project_id: 321, + name: 'test', + system_user_id: 1, + project_role_id: 2, + project_participation_id: 3 + }); + }); it('getProjectAttachments works as expected', async () => { mock.onGet(`/api/project/${projectId}/attachments/list`).reply(200, { @@ -67,14 +109,6 @@ describe('useProjectApi', () => { expect(result).toEqual(1); }); - it('getAttachmentSignedURL works as expected', async () => { - mock.onGet(`/api/project/${projectId}/attachments/${attachmentId}/getSignedUrl`).reply(200, 'www.signedurl.com'); - - const result = await useProjectApi(axios).getAttachmentSignedURL(projectId, attachmentId); - - expect(result).toEqual('www.signedurl.com'); - }); - it('getProjectsList works as expected', async () => { const response = [ { @@ -94,7 +128,7 @@ describe('useProjectApi', () => { } ]; - mock.onPost(`/api/projects`).reply(200, response); + mock.onGet(`/api/project/list`).reply(200, response); const result = await useProjectApi(axios).getProjectsList(); @@ -120,7 +154,7 @@ describe('useProjectApi', () => { } ]; - mock.onGet(`/api/public/projects`).reply(200, response); + mock.onGet(`/api/public/project/list`).reply(200, response); const result = await usePublicProjectApi(axios).getProjectsList(); @@ -225,7 +259,7 @@ describe('useProjectApi', () => { mock.onPost(`/api/project/${projectId}/attachments/upload`).reply(200, 'result 1'); - const result = await useProjectApi(axios).uploadProjectAttachments(projectId, file, attachmentType); + const result = await useProjectApi(axios).uploadProjectAttachments(projectId, file, attachmentType, attachmentMeta); expect(result).toEqual('result 1'); }); @@ -242,7 +276,7 @@ describe('useProjectApi', () => { partnerships: (null as unknown) as IProjectPartnershipsForm }; - mock.onPost('/api/project').reply(200, { + mock.onPost('/api/project/create').reply(200, { id: 1 }); @@ -261,12 +295,26 @@ describe('useProjectApi', () => { expect(result).toEqual({ id: 1 }); }); - it('getAttachmentSignedURL works as expected', async () => { + it('getAttachmentSignedURL works as expected for public access', async () => { + mock + .onGet(`/api/public/project/${projectId}/attachments/${attachmentId}/getSignedUrl`, { + query: { attachmentType: 'Other' } + }) + .reply(200, 'www.signedurl.com'); + + const result = await usePublicProjectApi(axios).getAttachmentSignedURL(projectId, attachmentId, 'Other'); + + expect(result).toEqual('www.signedurl.com'); + }); + + it('getAttachmentSignedURL works as expected for authenticated access', async () => { mock - .onPost(`/api/public/project/${projectId}/attachments/${attachmentId}/getSignedUrl`) + .onGet(`/api/project/${projectId}/attachments/${attachmentId}/getSignedUrl`, { + query: { attachmentType: 'Other' } + }) .reply(200, 'www.signedurl.com'); - const result = await usePublicProjectApi(axios).getAttachmentSignedURL(projectId, attachmentId, 'Image'); + const result = await useProjectApi(axios).getAttachmentSignedURL(projectId, attachmentId, 'Other'); expect(result).toEqual('www.signedurl.com'); }); @@ -294,4 +342,68 @@ describe('useProjectApi', () => { } ]); }); + + it('updateProjectAttachmentMetadata works as expected', async () => { + mock.onPut(`/api/project/${projectId}/attachments/${attachmentId}/metadata/update`).reply(200, 'result 1'); + + const result = await useProjectApi(axios).updateProjectReportMetadata( + projectId, + attachmentId, + attachmentMetaForUpdate, + attachmentMetaForUpdate.revision_count + ); + + expect(result).toEqual('result 1'); + }); + + it('getProjectReportMetadata works as expected', async () => { + mock.onGet(`/api/project/${projectId}/attachments/${attachmentId}/metadata/get`).reply(200, 'result 1'); + + const result = await useProjectApi(axios).getProjectReportMetadata(projectId, attachmentId); + + expect(result).toEqual('result 1'); + }); + + it('getProjectParticipants works as expected', async () => { + const mockResponse = { participants: [] }; + mock.onGet(`/api/project/${projectId}/participants/get`).reply(200, mockResponse); + + const result = await useProjectApi(axios).getProjectParticipants(projectId); + + expect(result).toEqual(mockResponse); + }); + + it('addProjectParticipants works as expected', async () => { + const mockResponse = { participants: [] }; + mock.onGet(`/api/project/${projectId}/participants/get`).reply(200, mockResponse); + + const result = await useProjectApi(axios).getProjectParticipants(projectId); + + expect(result).toEqual(mockResponse); + }); + + it('removeProjectParticipant works as expected', async () => { + const projectParticipationId = 1; + + mock.onDelete(`/api/project/${projectId}/participants/${projectParticipationId}/delete`).reply(200); + + const result = await useProjectApi(axios).removeProjectParticipant(projectId, projectParticipationId); + + expect(result).toEqual(true); + }); + + it('removeProjectParticipant works as expected', async () => { + const projectParticipationId = 1; + const projectRoleId = 1; + + mock.onPut(`/api/project/${projectId}/participants/${projectParticipationId}/update`).reply(200); + + const result = await useProjectApi(axios).updateProjectParticipantRole( + projectId, + projectParticipationId, + projectRoleId + ); + + expect(result).toEqual(true); + }); }); diff --git a/app/src/hooks/api/useProjectApi.ts b/app/src/hooks/api/useProjectApi.ts index c219848497..62927e4c1a 100644 --- a/app/src/hooks/api/useProjectApi.ts +++ b/app/src/hooks/api/useProjectApi.ts @@ -1,13 +1,20 @@ import { AxiosInstance, CancelTokenSource } from 'axios'; +import { IEditReportMetaForm } from 'components/attachments/EditReportMetaForm'; +import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; import { + IAddProjectParticipant, ICreateProjectRequest, ICreateProjectResponse, IGetProjectAttachmentsResponse, IGetProjectForUpdateResponse, IGetProjectForViewResponse, + IGetProjectParticipantsResponse, IGetProjectsListResponse, + IGetReportMetaData, + IGetUserProjectsListResponse, IProjectAdvancedFilterRequest, IUpdateProjectRequest, + IUploadAttachmentResponse, UPDATE_GET_ENTITIES } from 'interfaces/useProjectApi.interface'; import qs from 'qs'; @@ -19,6 +26,17 @@ import qs from 'qs'; * @return {*} object whose properties are supported api methods. */ const useProjectApi = (axios: AxiosInstance) => { + /** + * Get projects from userId + * + * @param {number} userId + * @return {*} {Promise} + */ + const getAllUserProjectsForView = async (userId: number): Promise => { + const { data } = await axios.get(`/api/user/${userId}/projects/get`); + return data; + }; + /** * Get project attachments based on project ID * @@ -56,7 +74,7 @@ const useProjectApi = (axios: AxiosInstance) => { projectId: number, attachmentId: number, attachmentType: string, - securityToken: any + securityToken: string ): Promise => { const { data } = await axios.post(`/api/project/${projectId}/attachments/${attachmentId}/delete`, { attachmentType, @@ -69,11 +87,22 @@ const useProjectApi = (axios: AxiosInstance) => { /** * Get project attachment S3 url based on project and attachment ID * - * @param {AxiosInstance} axios - * @returns {*} {Promise} + * @param {number} projectId + * @param {number} attachmentId + * @param {string} attachmentType + * @return {*} {Promise} */ - const getAttachmentSignedURL = async (projectId: number, attachmentId: number): Promise => { - const { data } = await axios.get(`/api/project/${projectId}/attachments/${attachmentId}/getSignedUrl`); + const getAttachmentSignedURL = async ( + projectId: number, + attachmentId: number, + attachmentType: string + ): Promise => { + const { data } = await axios.get(`/api/project/${projectId}/attachments/${attachmentId}/getSignedUrl`, { + params: { attachmentType: attachmentType }, + paramsSerializer: (params: any) => { + return qs.stringify(params); + } + }); return data; }; @@ -87,7 +116,15 @@ const useProjectApi = (axios: AxiosInstance) => { const getProjectsList = async ( filterFieldData?: IProjectAdvancedFilterRequest ): Promise => { - const { data } = await axios.post(`/api/projects`, filterFieldData || {}); + const { data } = await axios.get(`/api/project/list`, { + params: filterFieldData, + paramsSerializer: (params: any) => { + return qs.stringify(params, { + arrayFormat: 'repeat', + filter: (_prefix: any, value: any) => value || undefined + }); + } + }); return data; }; @@ -116,7 +153,7 @@ const useProjectApi = (axios: AxiosInstance) => { ): Promise => { const { data } = await axios.get(`api/project/${projectId}/update`, { params: { entity: entities }, - paramsSerializer: (params) => { + paramsSerializer: (params: any) => { return qs.stringify(params); } }); @@ -144,7 +181,7 @@ const useProjectApi = (axios: AxiosInstance) => { * @return {*} {Promise} */ const createProject = async (project: ICreateProjectRequest): Promise => { - const { data } = await axios.post('/api/project', project); + const { data } = await axios.post('/api/project/create', project); return data; }; @@ -162,14 +199,12 @@ const useProjectApi = (axios: AxiosInstance) => { const uploadProjectAttachments = async ( projectId: number, file: File, - attachmentType: string, cancelTokenSource?: CancelTokenSource, onProgress?: (progressEvent: ProgressEvent) => void - ): Promise => { + ): Promise => { const req_message = new FormData(); req_message.append('media', file); - req_message.append('attachmentType', attachmentType); const { data } = await axios.post(`/api/project/${projectId}/attachments/upload`, req_message, { cancelToken: cancelTokenSource?.token, @@ -179,6 +214,76 @@ const useProjectApi = (axios: AxiosInstance) => { return data; }; + /** + * Upload project reports. + * + * @param {number} projectId + * @param {File} file + * @param {IReportMetaForm} attachmentMeta + * @param {CancelTokenSource} [cancelTokenSource] + * @param {(progressEvent: ProgressEvent) => void} [onProgress] + * @return {*} {Promise} + */ + const uploadProjectReports = async ( + projectId: number, + file: File, + attachmentMeta: IReportMetaForm, + cancelTokenSource?: CancelTokenSource, + onProgress?: (progressEvent: ProgressEvent) => void + ): Promise => { + const req_message = new FormData(); + + req_message.append('media', file); + + if (attachmentMeta) { + req_message.append('attachmentMeta[title]', attachmentMeta.title); + req_message.append('attachmentMeta[year_published]', String(attachmentMeta.year_published)); + req_message.append('attachmentMeta[description]', attachmentMeta.description); + attachmentMeta.authors.forEach((authorObj, index) => { + req_message.append(`attachmentMeta[authors][${index}][first_name]`, authorObj.first_name); + req_message.append(`attachmentMeta[authors][${index}][last_name]`, authorObj.last_name); + }); + } + + const { data } = await axios.post(`/api/project/${projectId}/attachments/report/upload`, req_message, { + cancelToken: cancelTokenSource?.token, + onUploadProgress: onProgress + }); + + return data; + }; + + /** + * Update project attachment metadata. + * + * @param {number} projectId + * @param {string} attachmentType + * @param {CancelTokenSource} [cancelTokenSource] + * @param {(progressEvent: ProgressEvent) => void} [onProgress] + * @return {*} {Promise} + */ + const updateProjectReportMetadata = async ( + projectId: number, + attachmentId: number, + attachmentType: string, + attachmentMeta: IEditReportMetaForm, + revisionCount: number + ): Promise => { + const obj = { + attachment_type: attachmentType, + attachment_meta: { + title: attachmentMeta.title, + year_published: attachmentMeta.year_published, + authors: attachmentMeta.authors, + description: attachmentMeta.description + }, + revision_count: revisionCount + }; + + const { data } = await axios.put(`/api/project/${projectId}/attachments/${attachmentId}/metadata/update`, obj); + return data; + }; + /** * Make security status of project attachment secure. * @@ -211,7 +316,7 @@ const useProjectApi = (axios: AxiosInstance) => { const makeAttachmentUnsecure = async ( projectId: number, attachmentId: number, - securityToken: any, + securityToken: string, attachmentType: string ): Promise => { const { data } = await axios.put(`/api/project/${projectId}/attachments/${attachmentId}/makeUnsecure`, { @@ -227,9 +332,9 @@ const useProjectApi = (axios: AxiosInstance) => { * * @param {number} projectId * @param {number} pfsId - * @returns {*} {Promise} + * @return {*} {Promise} */ - const deleteFundingSource = async (projectId: number, pfsId: number): Promise => { + const deleteFundingSource = async (projectId: number, pfsId: number): Promise => { const { data } = await axios.delete(`/api/project/${projectId}/funding-sources/${pfsId}/delete`); return data; @@ -239,9 +344,10 @@ const useProjectApi = (axios: AxiosInstance) => { * Add new funding source based on projectId * * @param {number} projectId - * @returns {*} {Promise} + * @param {*} fundingSource + * @return {*} {Promise} */ - const addFundingSource = async (projectId: number, fundingSource: any): Promise => { + const addFundingSource = async (projectId: number, fundingSource: any): Promise => { const { data } = await axios.post(`/api/project/${projectId}/funding-sources/add`, fundingSource); return data; @@ -259,11 +365,94 @@ const useProjectApi = (axios: AxiosInstance) => { return data; }; + /** + * Get project report metadata based on project ID, attachment ID, and attachmentType + * + * @param {number} projectId + * @param {number} attachmentId + * @param {string} attachmentType + * @return {*} {Promise} + */ + const getProjectReportMetadata = async (projectId: number, attachmentId: number): Promise => { + const { data } = await axios.get(`/api/project/${projectId}/attachments/${attachmentId}/metadata/get`, { + params: {}, + paramsSerializer: (params: any) => { + return qs.stringify(params); + } + }); + + return data; + }; + + /** + * Get all project participants. + * + * @param {number} projectId + * @return {*} {Promise} + */ + const getProjectParticipants = async (projectId: number): Promise => { + const { data } = await axios.get(`/api/project/${projectId}/participants/get`); + + return data; + }; + + /** + * Add new project participants. + * + * @param {number} projectId + * @param {IAddProjectParticipant[]} participants + * @return {*} {Promise} `true` if the request was successful, false otherwise. + */ + const addProjectParticipants = async ( + projectId: number, + participants: IAddProjectParticipant[] + ): Promise => { + const { status } = await axios.post(`/api/project/${projectId}/participants/create`, { participants }); + + return status === 200; + }; + + /** + * Remove existing project participant. + * + * @param {number} projectId + * @param {number} projectParticipationId + * @return {*} {Promise} `true` if the request was successful, false otherwise. + */ + const removeProjectParticipant = async (projectId: number, projectParticipationId: number): Promise => { + const { status } = await axios.delete(`/api/project/${projectId}/participants/${projectParticipationId}/delete`); + + return status === 200; + }; + + /** + * Update project participant role. + * + * @param {number} projectId + * @param {number} projectParticipationId + * @param {string} role + * @return {*} {Promise} + */ + const updateProjectParticipantRole = async ( + projectId: number, + projectParticipationId: number, + roleId: number + ): Promise => { + const { status } = await axios.put(`/api/project/${projectId}/participants/${projectParticipationId}/update`, { + roleId + }); + + return status === 200; + }; + return { + getAllUserProjectsForView, getProjectsList, createProject, getProjectForView, uploadProjectAttachments, + uploadProjectReports, + updateProjectReportMetadata, getProjectForUpdate, updateProject, getProjectAttachments, @@ -274,7 +463,12 @@ const useProjectApi = (axios: AxiosInstance) => { deleteProject, publishProject, makeAttachmentSecure, - makeAttachmentUnsecure + makeAttachmentUnsecure, + getProjectReportMetadata, + getProjectParticipants, + addProjectParticipants, + removeProjectParticipant, + updateProjectParticipantRole }; }; @@ -293,7 +487,7 @@ export const usePublicProjectApi = (axios: AxiosInstance) => { * @return {*} {Promise} */ const getProjectsList = async (): Promise => { - const { data } = await axios.get(`/api/public/projects`); + const { data } = await axios.get(`/api/public/project/list`); return data; }; @@ -335,8 +529,33 @@ export const usePublicProjectApi = (axios: AxiosInstance) => { attachmentId: number, attachmentType: string ): Promise => { - const { data } = await axios.post(`/api/public/project/${projectId}/attachments/${attachmentId}/getSignedUrl`, { - attachmentType + const { data } = await axios.get(`/api/public/project/${projectId}/attachments/${attachmentId}/getSignedUrl`, { + params: { attachmentType: attachmentType }, + paramsSerializer: (params: any) => { + return qs.stringify(params); + } + }); + + return data; + }; + + /** + * Get project report metadata based on project ID, attachment ID, and attachmentType + * + * @param {number} projectId + * @param {number} attachmentId + * @param {string} attachmentType + * @returns {*} {Promise} + */ + const getPublicProjectReportMetadata = async ( + projectId: number, + attachmentId: number + ): Promise => { + const { data } = await axios.get(`/api/public/project/${projectId}/attachments/${attachmentId}/metadata/get`, { + params: {}, + paramsSerializer: (params: any) => { + return qs.stringify(params); + } }); return data; @@ -346,6 +565,7 @@ export const usePublicProjectApi = (axios: AxiosInstance) => { getProjectsList, getProjectForView, getProjectAttachments, - getAttachmentSignedURL + getAttachmentSignedURL, + getPublicProjectReportMetadata }; }; diff --git a/app/src/hooks/api/useSurveyApi.test.ts b/app/src/hooks/api/useSurveyApi.test.ts index 03a1b04037..1cd9e771a7 100644 --- a/app/src/hooks/api/useSurveyApi.test.ts +++ b/app/src/hooks/api/useSurveyApi.test.ts @@ -1,7 +1,10 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { IEditReportMetaForm } from 'components/attachments/EditReportMetaForm'; +import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; import { ICreateSurveyRequest, UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; import { getSurveyForViewResponse } from 'test-helpers/survey-helpers'; +import { AttachmentType } from '../../constants/attachments'; import useSurveyApi from './useSurveyApi'; describe('useSurveyApi', () => { @@ -19,6 +22,23 @@ describe('useSurveyApi', () => { const surveyId = 2; const attachmentId = 3; const attachmentType = 'type'; + const attachmentMeta: IReportMetaForm = { + title: 'upload file', + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'file abstract', + year_published: 2000, + attachmentFile: new File(['foo'], 'foo.txt', { + type: 'text/plain' + }) + }; + + const attachmentMetaForUpdate: IEditReportMetaForm = { + title: 'upload file', + authors: [{ first_name: 'John', last_name: 'Smith' }], + description: 'file abstract', + year_published: 2000, + revision_count: 1 + }; it('getObservationSubmissionSignedURL works as expected', async () => { const submissionId = 4; @@ -129,10 +149,17 @@ describe('useSurveyApi', () => { const signedUrl = 'url.com'; mock - .onGet(`/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/getSignedUrl`) + .onGet(`/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/getSignedUrl`, { + query: { attachmentType: AttachmentType.REPORT } + }) .reply(200, signedUrl); - const result = await useSurveyApi(axios).getSurveyAttachmentSignedURL(projectId, surveyId, attachmentId); + const result = await useSurveyApi(axios).getSurveyAttachmentSignedURL( + projectId, + surveyId, + attachmentId, + AttachmentType.REPORT + ); expect(result).toEqual(signedUrl); }); @@ -163,7 +190,19 @@ describe('useSurveyApi', () => { mock.onPost(`/api/project/${projectId}/survey/${surveyId}/attachments/upload`).reply(200, 'result 1'); - const result = await useSurveyApi(axios).uploadSurveyAttachments(projectId, surveyId, file, attachmentType); + const result = await useSurveyApi(axios).uploadSurveyAttachments(projectId, surveyId, file); + + expect(result).toEqual('result 1'); + }); + + it('uploadSurveyReport works as expected', async () => { + const file = new File(['foo'], 'foo.txt', { + type: 'text/plain' + }); + + mock.onPost(`/api/project/${projectId}/survey/${surveyId}/attachments/report/upload`).reply(200, 'result 1'); + + const result = await useSurveyApi(axios).uploadSurveyReports(projectId, surveyId, file, attachmentMeta); expect(result).toEqual('result 1'); }); @@ -270,11 +309,11 @@ describe('useSurveyApi', () => { }); it('publishSurvey works as expected', async () => { - mock.onPut(`/api/project/${projectId}/survey/${surveyId}/publish`).reply(200, 'OK'); + mock.onPut(`/api/project/${projectId}/survey/${surveyId}/publish`).reply(200, true); const result = await useSurveyApi(axios).publishSurvey(projectId, surveyId, true); - expect(result).toEqual('OK'); + expect(result).toEqual(true); }); it('deleteSummarySubmission works as expected', async () => { @@ -322,4 +361,31 @@ describe('useSurveyApi', () => { expect(result).toEqual(true); }); + + it('updateSurveyReportMetadata works as expected', async () => { + mock + .onPut(`/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/metadata/update`) + .reply(200, 'result 1'); + + const result = await useSurveyApi(axios).updateSurveyReportMetadata( + projectId, + surveyId, + attachmentId, + attachmentType, + attachmentMetaForUpdate, + attachmentMetaForUpdate.revision_count + ); + + expect(result).toEqual('result 1'); + }); + + it('getSurveyReportMetadata works as expected', async () => { + mock + .onGet(`/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/metadata/get`) + .reply(200, 'result 1'); + + const result = await useSurveyApi(axios).getSurveyReportMetadata(projectId, surveyId, attachmentId); + + expect(result).toEqual('result 1'); + }); }); diff --git a/app/src/hooks/api/useSurveyApi.ts b/app/src/hooks/api/useSurveyApi.ts index 62358082c2..10dd06be0f 100644 --- a/app/src/hooks/api/useSurveyApi.ts +++ b/app/src/hooks/api/useSurveyApi.ts @@ -1,20 +1,21 @@ import { AxiosInstance, CancelTokenSource } from 'axios'; +import { IEditReportMetaForm } from 'components/attachments/EditReportMetaForm'; +import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; +import { IGetSubmissionCSVForViewResponse } from 'interfaces/useObservationApi.interface'; +import { IGetReportMetaData, IUploadAttachmentResponse } from 'interfaces/useProjectApi.interface'; import { IGetSummaryResultsResponse, IUploadSummaryResultsResponse } from 'interfaces/useSummaryResultsApi.interface'; import { ICreateSurveyRequest, ICreateSurveyResponse, + IGetSurveyAttachmentsResponse, + IGetSurveyForUpdateResponse, IGetSurveyForViewResponse, IGetSurveysListResponse, IUpdateSurveyRequest, - IGetSurveyForUpdateResponse, - UPDATE_GET_SURVEY_ENTITIES, - IGetSurveyAttachmentsResponse, + SurveyFundingSources, SurveyPermits, - SurveyFundingSources + UPDATE_GET_SURVEY_ENTITIES } from 'interfaces/useSurveyApi.interface'; - -import { IGetSubmissionCSVForViewResponse } from 'interfaces/useObservationApi.interface'; - import qs from 'qs'; /** @@ -112,14 +113,12 @@ const useSurveyApi = (axios: AxiosInstance) => { projectId: number, surveyId: number, file: File, - attachmentType: string, cancelTokenSource?: CancelTokenSource, onProgress?: (progressEvent: ProgressEvent) => void - ): Promise => { + ): Promise => { const req_message = new FormData(); req_message.append('media', file); - req_message.append('attachmentType', attachmentType); const { data } = await axios.post(`/api/project/${projectId}/survey/${surveyId}/attachments/upload`, req_message, { cancelToken: cancelTokenSource?.token, @@ -129,6 +128,89 @@ const useSurveyApi = (axios: AxiosInstance) => { return data; }; + /** + * Upload survey reports. + * + * @param {number} projectId + * @param {number} surveyId + * @param {File} file + * @param {string} attachmentType + * @param {CancelTokenSource} [cancelTokenSource] + * @param {(progressEvent: ProgressEvent) => void} [onProgress] + * @return {*} {Promise} + */ + const uploadSurveyReports = async ( + projectId: number, + surveyId: number, + file: File, + + attachmentMeta?: IReportMetaForm, + cancelTokenSource?: CancelTokenSource, + onProgress?: (progressEvent: ProgressEvent) => void + ): Promise => { + const req_message = new FormData(); + + req_message.append('media', file); + + if (attachmentMeta) { + req_message.append('attachmentMeta[title]', attachmentMeta.title); + req_message.append('attachmentMeta[year_published]', String(attachmentMeta.year_published)); + req_message.append('attachmentMeta[description]', attachmentMeta.description); + attachmentMeta.authors.forEach((authorObj, index) => { + req_message.append(`attachmentMeta[authors][${index}][first_name]`, authorObj.first_name); + req_message.append(`attachmentMeta[authors][${index}][last_name]`, authorObj.last_name); + }); + } + + const { data } = await axios.post( + `/api/project/${projectId}/survey/${surveyId}/attachments/report/upload`, + req_message, + { + cancelToken: cancelTokenSource?.token, + onUploadProgress: onProgress + } + ); + + return data; + }; + + /** + * Update survey attachment metadata. + * + * @param {number} projectId + * @param {number} surveyId + * @param {string} attachmentType + * @param {CancelTokenSource} [cancelTokenSource] + * @param {(progressEvent: ProgressEvent) => void} [onProgress] + * @return {*} {Promise} + */ + const updateSurveyReportMetadata = async ( + projectId: number, + surveyId: number, + attachmentId: number, + attachmentType: string, + attachmentMeta: IEditReportMetaForm, + revisionCount: number + ): Promise => { + const obj = { + attachment_type: attachmentType, + attachment_meta: { + title: attachmentMeta.title, + year_published: attachmentMeta.year_published, + authors: attachmentMeta.authors, + description: attachmentMeta.description + }, + revision_count: revisionCount + }; + + const { data } = await axios.put( + `/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/metadata/update`, + obj + ); + + return data; + }; + /** * Get survey attachments based on survey ID * @@ -264,16 +346,26 @@ const useSurveyApi = (axios: AxiosInstance) => { /** * Get survey attachment S3 url based on survey and attachment ID * - * @param {AxiosInstance} axios + * @param {number} projectId + * @param {number} surveyId + * @param {number} attachmentId + * @param {string} attachmentType * @returns {*} {Promise} */ const getSurveyAttachmentSignedURL = async ( projectId: number, surveyId: number, - attachmentId: number + attachmentId: number, + attachmentType: string ): Promise => { const { data } = await axios.get( - `/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/getSignedUrl` + `/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/getSignedUrl`, + { + params: { attachmentType: attachmentType }, + paramsSerializer: (params) => { + return qs.stringify(params); + } + } ); return data; @@ -337,12 +429,12 @@ const useSurveyApi = (axios: AxiosInstance) => { * @param {number} projectId * @param {number} surveyId * @param {boolean} publish set to `true` to publish the survey, `false` to unpublish the survey. - * @return {*} {Promise} + * @return {*} {Promise} `true` if the request was successful, false otherwise. */ - const publishSurvey = async (projectId: number, surveyId: number, publish: boolean): Promise => { - const { data } = await axios.put(`/api/project/${projectId}/survey/${surveyId}/publish`, { publish: publish }); + const publishSurvey = async (projectId: number, surveyId: number, publish: boolean): Promise => { + const { status } = await axios.put(`/api/project/${projectId}/survey/${surveyId}/publish`, { publish: publish }); - return data; + return status === 200; }; /** @@ -412,6 +504,33 @@ const useSurveyApi = (axios: AxiosInstance) => { return data; }; + /** + * Get survey report metadata based on project ID, surveyID, attachment ID, and attachmentType + * + * @param {number} projectId + * @params {number} surveyId + * @param {number} attachmentId + * @param {string} attachmentType + * @returns {*} {Promise} + */ + const getSurveyReportMetadata = async ( + projectId: number, + surveyId: number, + attachmentId: number + ): Promise => { + const { data } = await axios.get( + `/api/project/${projectId}/survey/${surveyId}/attachments/${attachmentId}/metadata/get`, + { + params: {}, + paramsSerializer: (params) => { + return qs.stringify(params); + } + } + ); + + return data; + }; + return { createSurvey, getSurveyForView, @@ -419,6 +538,9 @@ const useSurveyApi = (axios: AxiosInstance) => { getSurveyForUpdate, updateSurvey, uploadSurveyAttachments, + uploadSurveyReports, + updateSurveyReportMetadata, + getSurveyReportMetadata, uploadSurveySummaryResults, getSurveySummarySubmission, getSurveyAttachments, diff --git a/app/src/hooks/api/useTaxonomyApi.test.ts b/app/src/hooks/api/useTaxonomyApi.test.ts new file mode 100644 index 0000000000..b4afb5419d --- /dev/null +++ b/app/src/hooks/api/useTaxonomyApi.test.ts @@ -0,0 +1,53 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import useTaxonomyApi from './useTaxonomyApi'; + +describe('useTaxonomyApi', () => { + let mock: any; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + it('searchSpecies works as expected', async () => { + const res = [ + { + id: '1', + label: 'something' + }, + { + id: '2', + label: 'anything' + } + ]; + + mock.onGet('/api/taxonomy/species/search').reply(200, res); + + const result = await useTaxonomyApi(axios).searchSpecies('th'); + + expect(result).toEqual(res); + }); + + it('getSpeciesFromIds works as expected', async () => { + const res = [ + { + id: '1', + label: 'something' + }, + { + id: '2', + label: 'anything' + } + ]; + + mock.onGet('/api/taxonomy/species/list').reply(200, res); + + const result = await useTaxonomyApi(axios).getSpeciesFromIds({ searchResponse: [1, 2] }); + + expect(result).toEqual(res); + }); +}); diff --git a/app/src/hooks/api/useTaxonomyApi.ts b/app/src/hooks/api/useTaxonomyApi.ts new file mode 100644 index 0000000000..8b016635e0 --- /dev/null +++ b/app/src/hooks/api/useTaxonomyApi.ts @@ -0,0 +1,27 @@ +import { AxiosInstance } from 'axios'; +import qs from 'qs'; + +const useTaxonomyApi = (axios: AxiosInstance): any => { + const searchSpecies = async (value: string): Promise => { + axios.defaults.params = { terms: value }; + + const { data } = await axios.get(`/api/taxonomy/species/search`); + + return data; + }; + + const getSpeciesFromIds = async (value: number[]): Promise => { + axios.defaults.params = { ids: qs.stringify(value) }; + + const { data } = await axios.get(`/api/taxonomy/species/list`); + + return data; + }; + + return { + searchSpecies, + getSpeciesFromIds + }; +}; + +export default useTaxonomyApi; diff --git a/app/src/hooks/api/useUserApi.test.ts b/app/src/hooks/api/useUserApi.test.ts index 2dc073388c..42f667bd56 100644 --- a/app/src/hooks/api/useUserApi.test.ts +++ b/app/src/hooks/api/useUserApi.test.ts @@ -13,6 +13,8 @@ describe('useUserApi', () => { mock.restore(); }); + const userId = 123; + it('getUser works as expected', async () => { mock.onGet('/api/user/self').reply(200, { id: 1, @@ -27,8 +29,24 @@ describe('useUserApi', () => { expect(result.role_names).toEqual(['role 1', 'role 2']); }); + it('getUserById works as expected', async () => { + mock.onGet(`/api/user/${userId}/get`).reply(200, { + id: 123, + user_record_end_date: 'test', + user_identifier: 'myidirboss', + role_names: ['role 1', 'role 2'] + }); + + const result = await useUserApi(axios).getUserById(123); + + expect(result.id).toEqual(123); + expect(result.user_record_end_date).toEqual('test'); + expect(result.user_identifier).toEqual('myidirboss'); + expect(result.role_names).toEqual(['role 1', 'role 2']); + }); + it('getUsersList works as expected', async () => { - mock.onGet('/api/users').reply(200, [ + mock.onGet('/api/user/list').reply(200, [ { id: 1, user_identifier: 'myidirboss', @@ -50,4 +68,14 @@ describe('useUserApi', () => { expect(result[1].user_identifier).toEqual('myidirbossagain'); expect(result[1].role_names).toEqual(['role 1', 'role 4']); }); + + it('addSystemUserRoles works as expected', async () => { + const userId = 1; + + mock.onPost(`/api/user/${userId}/system-roles/create`).reply(200, 3); + + const result = await useUserApi(axios).addSystemUserRoles(1, [1, 2, 3]); + + expect(result).toEqual(3); + }); }); diff --git a/app/src/hooks/api/useUserApi.ts b/app/src/hooks/api/useUserApi.ts index a9c8e9fc81..128d64b7e7 100644 --- a/app/src/hooks/api/useUserApi.ts +++ b/app/src/hooks/api/useUserApi.ts @@ -19,20 +19,70 @@ const useUserApi = (axios: AxiosInstance) => { return data; }; + /** + * Get user from userId + * + * @param {number} userId + * @return {*} {Promise} + */ + const getUserById = async (userId: number): Promise => { + const { data } = await axios.get(`/api/user/${userId}/get`); + return data; + }; + /** * Get user details for all users. * * @return {*} {Promise} */ const getUsersList = async (): Promise => { - const { data } = await axios.get('/api/users'); + const { data } = await axios.get('/api/user/list'); + + return data; + }; + + /** + * Get user details for all users. + * + * @return {*} {Promise} + */ + const deleteSystemUser = async (userId: number): Promise => { + const { data } = await axios.delete(`/api/user/${userId}/delete`); + + return data; + }; + + /** + * Grant one or more system roles to a user. + * + * @param {number} userId + * @param {number[]} roleIds + * @return {*} {Promise} + */ + const addSystemUserRoles = async (userId: number, roleIds: number[]): Promise => { + const { data } = await axios.post(`/api/user/${userId}/system-roles/create`, { roles: roleIds }); + + return data; + }; + + /** + * Get user details for all users. + * + * @return {*} {Promise} + */ + const updateSystemUserRoles = async (userId: number, roleIds: number[]): Promise => { + const { data } = await axios.patch(`/api/user/${userId}/system-roles/update`, { roles: roleIds }); return data; }; return { getUser, - getUsersList + getUserById, + getUsersList, + deleteSystemUser, + updateSystemUserRoles, + addSystemUserRoles }; }; diff --git a/app/src/hooks/useBioHubApi.ts b/app/src/hooks/useBioHubApi.ts index dd8e8818b3..af5085951b 100644 --- a/app/src/hooks/useBioHubApi.ts +++ b/app/src/hooks/useBioHubApi.ts @@ -1,18 +1,19 @@ import axios from 'axios'; +import { ConfigContext } from 'contexts/configContext'; +import { useContext } from 'react'; import useAdminApi from './api/useAdminApi'; import useAxios from './api/useAxios'; import useCodesApi from './api/useCodesApi'; import useDraftApi from './api/useDraftApi'; import useExternalApi from './api/useExternalApi'; +import useN8NApi from './api/useN8NApi'; +import useObservationApi from './api/useObservationApi'; +import usePermitApi from './api/usePermitApi'; import useProjectApi, { usePublicProjectApi } from './api/useProjectApi'; import useSearchApi, { usePublicSearchApi } from './api/useSearchApi'; import useSurveyApi from './api/useSurveyApi'; +import useTaxonomyApi from './api/useTaxonomyApi'; import useUserApi from './api/useUserApi'; -import usePermitApi from './api/usePermitApi'; -import useObservationApi from './api/useObservationApi'; -import { useContext } from 'react'; -import { ConfigContext } from 'contexts/configContext'; -import useN8NApi from './api/useN8NApi'; /** * Returns a set of supported api methods. @@ -30,6 +31,8 @@ export const useBiohubApi = () => { const search = useSearchApi(apiAxios); + const taxonomy = useTaxonomyApi(apiAxios); + const survey = useSurveyApi(apiAxios); const codes = useCodesApi(apiAxios); @@ -55,6 +58,7 @@ export const useBiohubApi = () => { project, permit, search, + taxonomy, survey, observation, codes, diff --git a/app/src/hooks/useCodes.ts b/app/src/hooks/useCodes.ts new file mode 100644 index 0000000000..06bb205622 --- /dev/null +++ b/app/src/hooks/useCodes.ts @@ -0,0 +1,43 @@ +import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; +import { useEffect, useState } from 'react'; +import { useBiohubApi } from './useBioHubApi'; + +export interface IUseCodes { + codes: IGetAllCodeSetsResponse | undefined; + isLoading: boolean; + isReady: boolean; +} + +/** + * Hook that fetches app code sets. + * + * @export + * @return {*} {IUseCodes} + */ +export default function useCodes(): IUseCodes { + const api = useBiohubApi(); + + const [codes, setCodes] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + const fetchCodes = async () => { + setIsLoading(true); + + const response = await api.codes.getAllCodeSets(); + + setCodes(response); + + setIsReady(true); + }; + + if (codes) { + return; + } + + fetchCodes(); + }, [api.codes, codes]); + + return { codes, isLoading, isReady }; +} diff --git a/app/src/hooks/useInterval.test.tsx b/app/src/hooks/useInterval.test.tsx new file mode 100644 index 0000000000..ad06d2ae54 --- /dev/null +++ b/app/src/hooks/useInterval.test.tsx @@ -0,0 +1,101 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { useInterval } from './useInterval'; + +interface ITestComponentProps { + callback: (() => any) | null | undefined; + period: number | null | undefined; + timeout: number; +} + +const TestComponent: React.FC = (props) => { + useInterval(props.callback, props.period, props.timeout); + + return <>; +}; + +describe('useInterval', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('calls the callback 5 times: once every 50 milliseconds for 250 milliseconds', async () => { + const callbackMock = jest.fn(); + + render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + jest.advanceTimersByTime(50); + expect(callbackMock.mock.calls.length).toEqual(3); // 150 milliseconds + + jest.advanceTimersByTime(850); + expect(callbackMock.mock.calls.length).toEqual(5); // 1000 milliseconds + }); + + it('stops calling the callback if the callback is updated to be falsy', async () => { + const callbackMock = jest.fn(); + + const { rerender } = render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + rerender(); + + jest.advanceTimersByTime(900); + expect(callbackMock.mock.calls.length).toEqual(2); // 1000 milliseconds + }); + + it('stops calling the callback if the period is updated to be falsy', async () => { + const callbackMock = jest.fn(); + + const { rerender } = render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + rerender(); + + jest.advanceTimersByTime(900); + expect(callbackMock.mock.calls.length).toEqual(2); // 1000 milliseconds + }); +}); diff --git a/app/src/hooks/useInterval.ts b/app/src/hooks/useInterval.ts index d0deb784ac..f6a7d55f07 100644 --- a/app/src/hooks/useInterval.ts +++ b/app/src/hooks/useInterval.ts @@ -1,18 +1,25 @@ -import { useRef, useEffect } from 'react'; +import { useEffect, useRef } from 'react'; /** - * Runs a `callback` function on a timer, once every `delay` milliseconds. + * Runs a `callback` function on a timer, once every `period` milliseconds. * - * Note: Does nothing if either `callback` or `delay` are null/undefined/falsy. + * Note: Does nothing if either `callback` or `period` are null/undefined/falsy. * - * Note: If both `callback` and `delay` are valid, the `callback` function will run for the first time after `delay` + * Note: If both `callback` and `period` are valid, the `callback` function will run for the first time after `period` * milliseconds (it will not run at time=0). * - * @param {(Function | null | undefined)} callback the function to run at each interval. Set to a falsy value to stop + * @param {((() => any) | null | undefined)} callback the function to run at each interval. Set to a falsy value to stop * the interval. - * @param {(number | null | undefined)} delay timer delay in milliseconds. Set to a falsy value to stop the interval. + * @param {(number | null | undefined)} period interval period in milliseconds. How often the `callback` should run. + * Set to a falsy value to stop the interval. + * @param {(number)} [timeout] timeout in milliseconds. The total polling time before the interval times out and + * automatically stops. */ -export const useInterval = (callback: Function | null | undefined, delay: number | null | undefined): void => { +export const useInterval = ( + callback: (() => any) | null | undefined, + period: number | null | undefined, + timeout?: number +): void => { const savedCallback = useRef(callback); useEffect(() => { @@ -20,12 +27,24 @@ export const useInterval = (callback: Function | null | undefined, delay: number }, [callback]); useEffect(() => { - if (!delay || !savedCallback?.current) { + if (!period || !savedCallback?.current) { return; } - const timeout = setInterval(() => savedCallback?.current?.(), delay); + const interval = setInterval(() => savedCallback?.current?.(), period); - return () => clearInterval(timeout); - }, [delay]); + let intervalTimeout: NodeJS.Timeout | undefined; + + if (timeout) { + intervalTimeout = setTimeout(() => clearInterval(interval), timeout); + } + + return () => { + clearInterval(interval); + + if (intervalTimeout) { + clearTimeout(intervalTimeout); + } + }; + }, [period, timeout]); }; diff --git a/app/src/hooks/useIsMounted.tsx b/app/src/hooks/useIsMounted.tsx index 73973bc507..8a58f0fc32 100644 --- a/app/src/hooks/useIsMounted.tsx +++ b/app/src/hooks/useIsMounted.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; /** * Use to track if a component is mounted/unmounted. diff --git a/app/src/hooks/useKeycloakWrapper.tsx b/app/src/hooks/useKeycloakWrapper.tsx index c58ff2fc51..ed9bab1bc1 100644 --- a/app/src/hooks/useKeycloakWrapper.tsx +++ b/app/src/hooks/useKeycloakWrapper.tsx @@ -4,6 +4,14 @@ import { KeycloakInstance } from 'keycloak-js'; import { useCallback, useEffect, useState } from 'react'; import { useBiohubApi } from './useBioHubApi'; +export enum SYSTEM_IDENTITY_SOURCE { + BCEID = 'BCEID', + IDIR = 'IDIR' +} + +const EXTERNAL_BCEID_IDENTITY_SOURCES = ['BCEID-BASIC-AND-BUSINESS', 'BCEID']; +const EXTERNAL_IDIR_IDENTITY_SOURCES = ['IDIR']; + /** * IUserInfo interface, represents the userinfo provided by keycloak. */ @@ -45,6 +53,12 @@ export interface IKeycloakWrapper { * @memberof IKeycloakWrapper */ systemRoles: string[]; + /** + * Returns `true` if the keycloak user is a registered system user, `false` otherwise. + * + * @memberof IKeycloakWrapper + */ + isSystemUser: () => boolean; /** * Returns `true` if the user's `systemRoles` contain at least 1 of the specified `validSystemRoles`, `false` otherwise. * @@ -126,16 +140,27 @@ function useKeycloakWrapper(): IKeycloakWrapper { /** * Parses out the identity source portion of the preferred_username from the token. * + * Note: Some identity sources have multiple variations (ie: BCEID) and are mapped to a single value supported by + * this app. + * * @param {object} keycloakToken * @return {*} {(string | null)} */ const getIdentitySource = useCallback((): string | null => { - const identitySource = keycloakUser?.['preferred_username']?.split('@')?.[1]; + const identitySource = keycloakUser?.['preferred_username']?.split('@')?.[1].toUpperCase(); if (!identitySource) { return null; } + if (EXTERNAL_BCEID_IDENTITY_SOURCES.includes(identitySource)) { + return SYSTEM_IDENTITY_SOURCE.BCEID; + } + + if (EXTERNAL_IDIR_IDENTITY_SOURCES.includes(identitySource)) { + return SYSTEM_IDENTITY_SOURCE.IDIR; + } + return identitySource; }, [keycloakUser]); @@ -145,10 +170,12 @@ function useKeycloakWrapper(): IKeycloakWrapper { try { userDetails = await biohubApi.user.getUser(); - } catch {} + } catch { + // do nothing + } setBioHubUser(() => { - if (userDetails?.role_names?.length) { + if (userDetails?.role_names?.length && !userDetails?.user_record_end_date) { setHasLoadedAllUserInfo(true); } else { setShouldLoadAccessRequest(true); @@ -177,7 +204,9 @@ function useKeycloakWrapper(): IKeycloakWrapper { try { accessRequests = await biohubApi.admin.hasPendingAdministrativeActivities(); - } catch {} + } catch { + // do nothing + } setHasAccessRequest(() => { setHasLoadedAllUserInfo(true); @@ -215,6 +244,10 @@ function useKeycloakWrapper(): IKeycloakWrapper { getKeycloakUser(); }, [keycloak, keycloakUser, isKeycloakUserLoading]); + const isSystemUser = (): boolean => { + return !!bioHubUser; + }; + const getSystemRoles = (): string[] => { return bioHubUser?.role_names || []; }; @@ -266,6 +299,7 @@ function useKeycloakWrapper(): IKeycloakWrapper { keycloak: keycloak, hasLoadedAllUserInfo, systemRoles: getSystemRoles(), + isSystemUser, hasSystemRole, hasAccessRequest, getUserIdentifier, diff --git a/app/src/index.tsx b/app/src/index.tsx index ddddfd1a7c..7fbc2000ce 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -1,6 +1,6 @@ +import App from 'App'; import React from 'react'; import ReactDOM from 'react-dom'; -import App from 'App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(, document.getElementById('root')); diff --git a/app/src/interfaces/useAdminApi.interface.ts b/app/src/interfaces/useAdminApi.interface.ts index 6f4f6f58d6..ebb0accd95 100644 --- a/app/src/interfaces/useAdminApi.interface.ts +++ b/app/src/interfaces/useAdminApi.interface.ts @@ -1,14 +1,19 @@ -export interface IAccessRequestDataObject { +export type IIDIRAccessRequestDataObject = { + role: number; + reason: string; +}; + +export type IBCeIDAccessRequestDataObject = { + company: string; + reason: string; +}; + +export type IAccessRequestDataObject = { name: string; username: string; email: string; identitySource: string; - role: number; - company: string; - regional_offices: number[]; - comments: string; - request_reason: string; -} +} & (IIDIRAccessRequestDataObject | IBCeIDAccessRequestDataObject); export interface IGetAccessRequestsListResponse { id: number; @@ -19,6 +24,18 @@ export interface IGetAccessRequestsListResponse { description: string; notes: string; create_date: string; - data: IAccessRequestDataObject; } + +export interface IgcNotifyGenericMessage { + header: string; + body1: string; + body2: string; + footer: string; +} + +export interface IgcNotifyRecipient { + emailAddress: string; + phoneNumber: string; + userId: number; +} diff --git a/app/src/interfaces/useCodesApi.interface.ts b/app/src/interfaces/useCodesApi.interface.ts index 2f9c1dba0f..8b5c6b9ada 100644 --- a/app/src/interfaces/useCodesApi.interface.ts +++ b/app/src/interfaces/useCodesApi.interface.ts @@ -29,13 +29,16 @@ export interface IGetAllCodeSetsResponse { activity: CodeSet; project_type: CodeSet; region: CodeSet; - species: CodeSet; - proprietor_type: CodeSet; + proprietor_type: CodeSet<{ id: number; name: string; is_first_nation: boolean }>; iucn_conservation_action_level_1_classification: CodeSet; iucn_conservation_action_level_2_subclassification: CodeSet<{ id: number; iucn1_id: number; name: string }>; iucn_conservation_action_level_3_subclassification: CodeSet<{ id: number; iucn2_id: number; name: string }>; system_roles: CodeSet; + project_roles: CodeSet; regional_offices: CodeSet; administrative_activity_status_type: CodeSet; - common_survey_methodologies: CodeSet; + field_methods: CodeSet<{ id: number; name: string; description: string }>; + intended_outcomes: CodeSet<{ id: number; name: string; description: string }>; + ecological_seasons: CodeSet<{ id: number; name: string; description: string }>; + vantage_codes: CodeSet; } diff --git a/app/src/interfaces/useProjectApi.interface.ts b/app/src/interfaces/useProjectApi.interface.ts index 40fe14be26..2194e898f6 100644 --- a/app/src/interfaces/useProjectApi.interface.ts +++ b/app/src/interfaces/useProjectApi.interface.ts @@ -14,7 +14,8 @@ export interface IGetProjectAttachment { fileType: string; lastModified: string; size: number; - securityToken: any; + securityToken: string; + revisionCount: number; } /** @@ -43,6 +44,20 @@ export interface IGetProjectAttachmentsResponse { attachmentsList: IGetProjectAttachment[]; } +/** + * Get projects list response object. + * + * @export + * @interface IGetUserProjectsListResponse + */ +export interface IGetUserProjectsListResponse { + project_id: number; + name: string; + system_user_id: number; + project_role_id: number; + project_participation_id: number; +} + /** * Get projects list response object. * @@ -193,7 +208,7 @@ export interface IGetProjectForUpdateResponsePartnerships { * @interface IUpdateProjectRequest * @extends {IGetProjectForUpdateResponse} */ -export interface IUpdateProjectRequest extends IGetProjectForUpdateResponse {} +export type IUpdateProjectRequest = IGetProjectForUpdateResponse; /** * An interface for a single instance of project metadata, for view-only use cases. @@ -215,7 +230,7 @@ export interface IGetProjectForViewResponse { export interface IGetProjectForViewResponseDetails { project_name: string; - project_type: string; + project_type: number; project_activities: number[]; start_date: string; end_date: string; @@ -251,9 +266,9 @@ export interface IGetProjectForViewResponseCoordinator { } interface IGetProjectForViewResponseIUCNArrayItem { - classification: string; - subClassification1: string; - subClassification2: string; + classification: number; + subClassification1: number; + subClassification2: number; } export interface IGetProjectForViewResponseIUCN { @@ -278,7 +293,7 @@ export interface IGetProjectForViewResponseFundingData { } export interface IGetProjectForViewResponsePartnerships { - indigenous_partnerships: string[]; + indigenous_partnerships: number[]; stakeholder_partnerships: string[]; } @@ -292,3 +307,48 @@ export interface IGetProjectMediaListResponse { file_name: string; encoded_file: string; } + +/** + * A file upload response. + * + * @export + * @interface IUploadAttachmentResponse + */ +export interface IUploadAttachmentResponse { + attachmentId: number; + revision_count: number; +} + +export interface IGetReportMetaData { + attachment_id: number; + title: string; + year_published: number; + description: string; + last_modified: string; + revision_count: number; + authors: IGetReportAuthors[]; +} + +export interface IGetReportAuthors { + first_name: string; + last_name: string; +} + +export interface IGetProjectParticipantsResponseArrayItem { + project_participation_id: number; + project_id: number; + system_user_id: number; + project_role_id: number; + project_role_name: string; + user_identifier: string; + user_identity_source_id: number; +} +export interface IGetProjectParticipantsResponse { + participants: IGetProjectParticipantsResponseArrayItem[]; +} + +export interface IAddProjectParticipant { + userIdentifier: string; + identitySource: string; + roleId: number; +} diff --git a/app/src/interfaces/useSurveyApi.interface.ts b/app/src/interfaces/useSurveyApi.interface.ts index 6ac80300ab..defb228930 100644 --- a/app/src/interfaces/useSurveyApi.interface.ts +++ b/app/src/interfaces/useSurveyApi.interface.ts @@ -1,4 +1,10 @@ +import { IAgreementsForm } from 'features/surveys/components/AgreementsForm'; +import { IGeneralInformationForm } from 'features/surveys/components/GeneralInformationForm'; +import { IProprietaryDataForm } from 'features/surveys/components/ProprietaryDataForm'; +import { IPurposeAndMethodologyForm } from 'features/surveys/components/PurposeAndMethodologyForm'; +import { IStudyAreaForm } from 'features/surveys/components/StudyAreaForm'; import { Feature } from 'geojson'; +import { StringBoolean } from 'types/misc'; /** * Create survey post object. @@ -6,28 +12,12 @@ import { Feature } from 'geojson'; * @export * @interface ICreateSurveyRequest */ -export interface ICreateSurveyRequest { - id: number; - biologist_first_name: string; - biologist_last_name: string; - category_rationale: string; - data_sharing_agreement_required: string; - end_date: string; - first_nations_id: number; - foippa_requirements_accepted: boolean; - proprietary_data_category: string; - proprietor_name: string; - sedis_procedures_accepted: boolean; - focal_species: number[]; - ancillary_species: number[]; - start_date: string; - survey_area_name: string; - survey_data_proprietary: string; - survey_name: string; - survey_purpose: string; - geometry: Feature[]; - permit_number: string; -} +export interface ICreateSurveyRequest + extends IGeneralInformationForm, + IPurposeAndMethodologyForm, + IStudyAreaForm, + IProprietaryDataForm, + IAgreementsForm {} /** * Create survey response object. @@ -50,10 +40,10 @@ export interface ISurveyFundingSourceForView { export interface IGetSurveyForViewResponseDetails { id: number; survey_name: string; - survey_purpose: string; - focal_species: string[]; - ancillary_species: string[]; - common_survey_methodology: string; + focal_species: number[]; + focal_species_names: string[]; + ancillary_species: number[]; + ancillary_species_names: string[]; start_date: string; end_date: string; biologist_first_name: string; @@ -68,6 +58,16 @@ export interface IGetSurveyForViewResponseDetails { occurrence_submission_id: number | null; } +export interface IGetSurveyForViewResponsePurposeAndMethodology { + id: number; + intended_outcome_id: number; + additional_details: string; + field_method_id: number; + ecological_season_id: number; + vantage_code_ids: number[]; + surveyed_all_areas: StringBoolean; +} + export interface IGetSurveyForViewResponseProprietor { id: number; proprietary_data_category_name: string; @@ -80,10 +80,8 @@ export interface IGetSurveyForViewResponseProprietor { export interface IGetSurveyForUpdateResponseDetails { id: number; survey_name: string; - survey_purpose: string; focal_species: number[]; ancillary_species: number[]; - common_survey_methodology_id: number; start_date: string; end_date: string; biologist_first_name: string; @@ -96,6 +94,17 @@ export interface IGetSurveyForUpdateResponseDetails { funding_sources: number[]; } +export interface IGetSurveyForUpdateResponsePurposeAndMethodology { + id?: number; + intended_outcome_id: number; + additional_details: string; + field_method_id: number; + ecological_season_id: number; + vantage_code_ids: number[]; + surveyed_all_areas: StringBoolean; + revision_count?: number; +} + export interface IGetSurveyForUpdateResponseProprietor { id?: number; proprietary_data_category_name?: string; @@ -117,6 +126,7 @@ export interface IGetSurveyForUpdateResponseProprietor { */ export interface IGetSurveyForUpdateResponse { survey_details?: IGetSurveyForUpdateResponseDetails; + survey_purpose_and_methodology?: IGetSurveyForUpdateResponsePurposeAndMethodology | null; survey_proprietor?: IGetSurveyForUpdateResponseProprietor | null; } @@ -128,6 +138,7 @@ export interface IGetSurveyForUpdateResponse { */ export interface IGetSurveyForViewResponse { survey_details: IGetSurveyForViewResponseDetails; + survey_purpose_and_methodology: IGetSurveyForViewResponsePurposeAndMethodology; survey_proprietor: IGetSurveyForViewResponseProprietor; } @@ -138,7 +149,7 @@ export interface IGetSurveyForViewResponse { * @interface IUpdateSurveyRequest * @extends {IGetSurveyForUpdateResponse} */ -export interface IUpdateSurveyRequest extends IGetSurveyForUpdateResponse {} +export type IUpdateSurveyRequest = IGetSurveyForUpdateResponse; /** * Get surveys list response object. @@ -147,17 +158,28 @@ export interface IUpdateSurveyRequest extends IGetSurveyForUpdateResponse {} * @interface IGetSurveysListResponse */ export interface IGetSurveysListResponse { + id: number; + survey: IGetSurveyDetailsResponse; + species: IGetSpeciesList; +} + +export interface IGetSurveyDetailsResponse { id: number; name: string; - species: string[]; start_date: string; end_date: string; publish_status: string; completion_status: string; } +export interface IGetSpeciesList { + species: number[]; + species_names: string[]; +} + export enum UPDATE_GET_SURVEY_ENTITIES { survey_details = 'survey_details', + survey_purpose_and_methodology = 'survey_purpose_and_methodology', survey_proprietor = 'survey_proprietor' } @@ -168,6 +190,7 @@ export interface IGetSurveyAttachment { lastModified: string; size: number; securityToken: any; + revisionCount: number; } /** diff --git a/app/src/interfaces/useUserApi.interface.ts b/app/src/interfaces/useUserApi.interface.ts index f99504cde4..f6cd89714f 100644 --- a/app/src/interfaces/useUserApi.interface.ts +++ b/app/src/interfaces/useUserApi.interface.ts @@ -1,5 +1,6 @@ export interface IGetUserResponse { id: number; + user_record_end_date: string; user_identifier: string; role_names: string[]; } diff --git a/app/src/layouts/PublicLayout.test.tsx b/app/src/layouts/PublicLayout.test.tsx index 285314cf65..7a7d813867 100644 --- a/app/src/layouts/PublicLayout.test.tsx +++ b/app/src/layouts/PublicLayout.test.tsx @@ -1,8 +1,8 @@ import { render } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import React from 'react'; -import PublicLayout from './PublicLayout'; import { Router } from 'react-router-dom'; -import { createMemoryHistory } from 'history'; +import PublicLayout from './PublicLayout'; const history = createMemoryHistory(); diff --git a/app/src/pages/200/RequestSubmitted.test.tsx b/app/src/pages/200/RequestSubmitted.test.tsx index 32b783f0cd..d86961d008 100644 --- a/app/src/pages/200/RequestSubmitted.test.tsx +++ b/app/src/pages/200/RequestSubmitted.test.tsx @@ -50,7 +50,7 @@ describe('RequestSubmitted', () => { const authState = { keycloakWrapper: { hasLoadedAllUserInfo: true, - systemRoles: [SYSTEM_ROLE.PROJECT_ADMIN], + systemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], hasAccessRequest: false, keycloak: {}, diff --git a/app/src/pages/403/AccessDenied.test.tsx b/app/src/pages/403/AccessDenied.test.tsx index 43071e75d2..81b4187d25 100644 --- a/app/src/pages/403/AccessDenied.test.tsx +++ b/app/src/pages/403/AccessDenied.test.tsx @@ -133,7 +133,7 @@ describe('AccessDenied', () => { hasLoadedAllUserInfo: true, hasAccessRequest: false, - systemRoles: [SYSTEM_ROLE.PROJECT_ADMIN], + systemRoles: [SYSTEM_ROLE.PROJECT_CREATOR], getUserIdentifier: jest.fn(), hasSystemRole: jest.fn(), getIdentitySource: jest.fn(), diff --git a/app/src/pages/404/NotFoundPage.tsx b/app/src/pages/404/NotFoundPage.tsx index d483ef955f..1bc2e5ac75 100644 --- a/app/src/pages/404/NotFoundPage.tsx +++ b/app/src/pages/404/NotFoundPage.tsx @@ -1,10 +1,10 @@ -import Container from '@material-ui/core/Container'; import Box from '@material-ui/core/Box'; -import React from 'react'; -import { mdiHelpCircleOutline } from '@mdi/js'; -import Icon from '@mdi/react'; import Button from '@material-ui/core/Button'; +import Container from '@material-ui/core/Container'; import Typography from '@material-ui/core/Typography'; +import { mdiHelpCircleOutline } from '@mdi/js'; +import Icon from '@mdi/react'; +import React from 'react'; import { useHistory } from 'react-router'; const NotFoundPage = () => { diff --git a/app/src/pages/access/AccessRequestPage.test.tsx b/app/src/pages/access/AccessRequestPage.test.tsx index c75c669fde..4dba567138 100644 --- a/app/src/pages/access/AccessRequestPage.test.tsx +++ b/app/src/pages/access/AccessRequestPage.test.tsx @@ -5,6 +5,7 @@ import { createMemoryHistory } from 'history'; import { useBiohubApi } from 'hooks/useBioHubApi'; import React from 'react'; import { Router } from 'react-router'; +import { getMockAuthState } from 'test-helpers/auth-helpers'; import AccessRequestPage from './AccessRequestPage'; const history = createMemoryHistory(); @@ -24,7 +25,7 @@ const mockBiohubApi = ((useBiohubApi as unknown) as jest.Mock { - const authState = { + const authState = getMockAuthState({ keycloakWrapper: { keycloak: { authenticated: true @@ -43,7 +44,7 @@ const renderContainer = () => { lastName: 'testlast', refresh: () => {} } - }; + }); return render( @@ -69,8 +70,7 @@ describe('AccessRequestPage', () => { it('renders correctly', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); const { asFragment } = renderContainer(); @@ -85,11 +85,10 @@ describe('AccessRequestPage', () => { it('should redirect to `/logout`', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); - const authState = { + const authState = getMockAuthState({ keycloakWrapper: { keycloak: { authenticated: true @@ -108,7 +107,7 @@ describe('AccessRequestPage', () => { lastName: 'testlast', refresh: () => {} } - }; + }); const { getByText } = render( @@ -126,32 +125,9 @@ describe('AccessRequestPage', () => { }); }); - // TODO Release 1 Patch (BHBC-1442): Remove regional offices - // it('shows and hides the regional offices section when the regional offices radio button is selected (respectively)', async () => { - // mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - // system_roles: [{ id: 1, name: 'Role 1' }], - // regional_offices: [{ id: 1, name: 'Office 1' }] - // }); - - // const { queryByText, getByText, getByTestId } = renderContainer(); - - // expect(queryByText('Which Regional Offices do you work for?')).toBeNull(); - - // fireEvent.click(getByTestId('yes-regional-office')); - - // await waitFor(() => { - // expect(getByText('Which Regional Offices do you work for?')).toBeInTheDocument(); - // }); - - // fireEvent.click(getByTestId('no-regional-office')); - - // expect(queryByText('Which Regional Offices do you work for?')).toBeNull(); - // }); - it('processes a successful request submission', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); mockBiohubApi().admin.createAdministrativeActivity.mockResolvedValue({ @@ -165,11 +141,11 @@ describe('AccessRequestPage', () => { const systemRoleListbox = within(getByRole('listbox')); await waitFor(() => { - expect(systemRoleListbox.getByText(/Role 1/i)).toBeInTheDocument(); + expect(systemRoleListbox.getByText('Creator')).toBeInTheDocument(); }); - fireEvent.click(systemRoleListbox.getByText(/Role 1/i)); - // fireEvent.click(getByTestId('no-regional-office')); // TODO Release 1 Patch (BHBC-1442): Remove regional offices + fireEvent.click(systemRoleListbox.getByText('Creator')); + fireEvent.click(getByText('Submit Request')); await waitFor(() => { @@ -179,11 +155,10 @@ describe('AccessRequestPage', () => { it('takes the user to the request-submitted page immediately if they already have an access request', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); - const authState = { + const authState = getMockAuthState({ keycloakWrapper: { keycloak: { authenticated: true @@ -202,7 +177,7 @@ describe('AccessRequestPage', () => { lastName: '', refresh: () => {} } - }; + }); render( @@ -219,8 +194,7 @@ describe('AccessRequestPage', () => { it('shows error dialog with api error message when submission fails', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); mockBiohubApi().admin.createAdministrativeActivity = jest.fn(() => Promise.reject(new Error('API Error is Here'))); @@ -232,11 +206,11 @@ describe('AccessRequestPage', () => { const systemRoleListbox = within(getByRole('listbox')); await waitFor(() => { - expect(systemRoleListbox.getByText(/Role 1/i)).toBeInTheDocument(); + expect(systemRoleListbox.getByText('Creator')).toBeInTheDocument(); }); - fireEvent.click(systemRoleListbox.getByText(/Role 1/i)); - // fireEvent.click(getByTestId('no-regional-office')); // TODO Release 1 Patch (BHBC-1442): Remove regional offices + fireEvent.click(systemRoleListbox.getByText('Creator')); + fireEvent.click(getByText('Submit Request')); await waitFor(() => { @@ -252,8 +226,7 @@ describe('AccessRequestPage', () => { it('shows error dialog with default error message when response from createAdministrativeActivity is invalid', async () => { mockBiohubApi().codes.getAllCodeSets.mockResolvedValue({ - system_roles: [{ id: 1, name: 'Role 1' }], - regional_offices: [{ id: 1, name: 'Office 1' }] + system_roles: [{ id: 1, name: 'Creator' }] }); mockBiohubApi().admin.createAdministrativeActivity.mockResolvedValue({ @@ -267,11 +240,11 @@ describe('AccessRequestPage', () => { const systemRoleListbox = within(getByRole('listbox')); await waitFor(() => { - expect(systemRoleListbox.getByText(/Role 1/i)).toBeInTheDocument(); + expect(systemRoleListbox.getByText('Creator')).toBeInTheDocument(); }); - fireEvent.click(systemRoleListbox.getByText(/Role 1/i)); - // fireEvent.click(getByTestId('no-regional-office')); // TODO Release 1 Patch (BHBC-1442): Remove regional offices + fireEvent.click(systemRoleListbox.getByText('Creator')); + fireEvent.click(getByText('Submit Request')); await waitFor(() => { diff --git a/app/src/pages/access/AccessRequestPage.tsx b/app/src/pages/access/AccessRequestPage.tsx index 1335fb141c..d9b921ccd4 100644 --- a/app/src/pages/access/AccessRequestPage.tsx +++ b/app/src/pages/access/AccessRequestPage.tsx @@ -2,9 +2,7 @@ import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import CircularProgress from '@material-ui/core/CircularProgress'; import Container from '@material-ui/core/Container'; -import Divider from '@material-ui/core/Divider'; import Paper from '@material-ui/core/Paper'; -import { Theme } from '@material-ui/core/styles/createMuiTheme'; import makeStyles from '@material-ui/core/styles/makeStyles'; import Typography from '@material-ui/core/Typography'; import { IErrorDialogProps } from 'components/dialog/ErrorDialog'; @@ -14,55 +12,23 @@ import { DialogContext } from 'contexts/dialogContext'; import { Formik } from 'formik'; import { APIError } from 'hooks/api/useAxios'; import { useBiohubApi } from 'hooks/useBioHubApi'; -import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; -import React, { useContext, useEffect, useState } from 'react'; +import useCodes from 'hooks/useCodes'; +import { SYSTEM_IDENTITY_SOURCE } from 'hooks/useKeycloakWrapper'; +import { IBCeIDAccessRequestDataObject, IIDIRAccessRequestDataObject } from 'interfaces/useAdminApi.interface'; +import React, { ReactElement, useContext, useState } from 'react'; import { Redirect, useHistory } from 'react-router'; import BCeIDRequestForm, { BCeIDRequestFormInitialValues, BCeIDRequestFormYupSchema } from './BCeIDRequestForm'; import IDIRRequestForm, { IDIRRequestFormInitialValues, IDIRRequestFormYupSchema } from './IDIRRequestForm'; -const useStyles = makeStyles((theme: Theme) => ({ +const useStyles = makeStyles(() => ({ actionButton: { minWidth: '6rem', '& + button': { marginLeft: '0.5rem' } - }, - breadCrumbLink: { - display: 'flex', - alignItems: 'center', - cursor: 'pointer' - }, - breadCrumbLinkIcon: { - marginRight: '0.25rem' - }, - finishContainer: { - padding: theme.spacing(3), - backgroundColor: 'transparent' - }, - stepper: { - backgroundColor: 'transparent' - }, - stepTitle: { - marginBottom: '0.45rem' - }, - spacingBottom: { - marginBottom: '0.9rem' - }, - legend: { - marginTop: '1rem', - float: 'left', - marginBottom: '0.75rem', - letterSpacing: '-0.01rem' } })); -interface IAccessRequestForm { - role: number; - work_from_regional_office: string; - regional_offices: number[]; - comments: string; -} - /** * Access Request form * @@ -70,8 +36,6 @@ interface IAccessRequestForm { */ export const AccessRequestPage: React.FC = () => { const classes = useStyles(); - const [codes, setCodes] = useState(); - const [isLoadingCodes, setIsLoadingCodes] = useState(false); const biohubApi = useBiohubApi(); const history = useHistory(); @@ -93,23 +57,7 @@ export const AccessRequestPage: React.FC = () => { const [isSubmittingRequest, setIsSubmittingRequest] = useState(false); - useEffect(() => { - const getAllCodeSets = async () => { - const response = await biohubApi.codes.getAllCodeSets(); - - // TODO error handling/user messaging - Cant submit an access request if required code sets fail to fetch - - setCodes(() => { - setIsLoadingCodes(false); - return response; - }); - }; - - if (!isLoadingCodes && !codes) { - getAllCodeSets(); - setIsLoadingCodes(true); - } - }, [biohubApi, isLoadingCodes, codes]); + const codes = useCodes(); const showAccessRequestErrorDialog = (textDialogProps?: Partial) => { dialogContext.setErrorDialog({ @@ -121,14 +69,14 @@ export const AccessRequestPage: React.FC = () => { }); }; - const handleSubmitAccessRequest = async (values: IAccessRequestForm) => { + const handleSubmitAccessRequest = async (values: IIDIRAccessRequestDataObject | IBCeIDAccessRequestDataObject) => { try { const response = await biohubApi.admin.createAdministrativeActivity({ ...values, - name: keycloakWrapper?.displayName, - username: keycloakWrapper?.getUserIdentifier(), - email: keycloakWrapper?.email, - identitySource: keycloakWrapper?.getIdentitySource() + name: keycloakWrapper?.displayName as string, + username: keycloakWrapper?.getUserIdentifier() as string, + email: keycloakWrapper?.email as string, + identitySource: keycloakWrapper?.getIdentitySource() as string }); if (!response?.id) { @@ -169,22 +117,22 @@ export const AccessRequestPage: React.FC = () => { return ; } - let initialValues: any; - let validationSchema: any; - let requestForm: any; + let initialValues: IIDIRAccessRequestDataObject | IBCeIDAccessRequestDataObject; + let validationSchema: typeof IDIRRequestFormYupSchema | typeof BCeIDRequestFormYupSchema; + let requestForm: ReactElement; - if (keycloakWrapper?.getIdentitySource()?.toLowerCase() === 'bceid') { + if (keycloakWrapper?.getIdentitySource() === SYSTEM_IDENTITY_SOURCE.BCEID) { initialValues = BCeIDRequestFormInitialValues; validationSchema = BCeIDRequestFormYupSchema; requestForm = ; } else { initialValues = IDIRRequestFormInitialValues; validationSchema = IDIRRequestFormYupSchema; - requestForm = ; + requestForm = ; } return ( - + { handleSubmitAccessRequest(values); }}> {({ handleSubmit }) => ( - <> - -

Request Access to SIMS

- - You will need to provide some additional details before accessing this application. Complete the form - below to request access. + + Request Access + + + You will need to provide some additional details before accessing this application. - -

Request Details

- - - {requestForm} - - - - - - - {isSubmittingRequest && ( - - )} - - {/* - CircularProgress styling examples: - https://codesandbox.io/s/wonderful-cartwright-e18nc?file=/demo.tsx:895-1013 - https://menubar.io/creating-a-material-ui-button-with-spinner-that-reflects-loading-state - */} - + {isSubmittingRequest && ( + { - history.push('/logout'); - }} - className={classes.actionButton} - data-testid="logout-button"> - Log out - - - + /> + )} + +
- +
- +
)}
diff --git a/app/src/pages/access/BCeIDRequestForm.test.tsx b/app/src/pages/access/BCeIDRequestForm.test.tsx index 376f9778f2..50039ee156 100644 --- a/app/src/pages/access/BCeIDRequestForm.test.tsx +++ b/app/src/pages/access/BCeIDRequestForm.test.tsx @@ -5,7 +5,7 @@ import BCeIDRequestForm, { BCeIDRequestFormInitialValues, BCeIDRequestFormYupSch describe('BCeIDRequestForm', () => { it('matches the snapshot', () => { - const { asFragment } = render( + const { getByTestId } = render( { ); - expect(asFragment()).toMatchSnapshot(); + expect(getByTestId('company')).toBeVisible(); + expect(getByTestId('reason')).toBeVisible(); }); }); diff --git a/app/src/pages/access/BCeIDRequestForm.tsx b/app/src/pages/access/BCeIDRequestForm.tsx index 16ab6bb2b0..318f14852d 100644 --- a/app/src/pages/access/BCeIDRequestForm.tsx +++ b/app/src/pages/access/BCeIDRequestForm.tsx @@ -1,22 +1,18 @@ import Box from '@material-ui/core/Box'; import Grid from '@material-ui/core/Grid'; import CustomTextField from 'components/fields/CustomTextField'; +import { IBCeIDAccessRequestDataObject } from 'interfaces/useAdminApi.interface'; import React from 'react'; import yup from 'utils/YupSchema'; -export interface IBCeIDRequestForm { - company: string; - request_reason: string; -} - -export const BCeIDRequestFormInitialValues: IBCeIDRequestForm = { +export const BCeIDRequestFormInitialValues: IBCeIDAccessRequestDataObject = { company: '', - request_reason: '' + reason: '' }; export const BCeIDRequestFormYupSchema = yup.object().shape({ company: yup.string().required('Required'), - request_reason: yup.string().max(300, 'Maximum 300 characters') + reason: yup.string().max(300, 'Maximum 300 characters') }); /** @@ -39,8 +35,8 @@ const BCeIDRequestForm = () => { /> -

Why are you requesting access to SIMS?

- +

Why are you requesting access to Species Inventory Management System (SIMS)?

+
diff --git a/app/src/pages/access/IDIRRequestForm.tsx b/app/src/pages/access/IDIRRequestForm.tsx index b7dc9d77fa..af5498e642 100644 --- a/app/src/pages/access/IDIRRequestForm.tsx +++ b/app/src/pages/access/IDIRRequestForm.tsx @@ -1,158 +1,80 @@ import Box from '@material-ui/core/Box'; import FormControl from '@material-ui/core/FormControl'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; import FormHelperText from '@material-ui/core/FormHelperText'; -import FormLabel from '@material-ui/core/FormLabel'; import Grid from '@material-ui/core/Grid'; import InputLabel from '@material-ui/core/InputLabel'; import MenuItem from '@material-ui/core/MenuItem'; -import Radio from '@material-ui/core/Radio'; -import RadioGroup from '@material-ui/core/RadioGroup'; import Select from '@material-ui/core/Select'; -import { Theme } from '@material-ui/core/styles/createMuiTheme'; -import makeStyles from '@material-ui/core/styles/makeStyles'; +import Typography from '@material-ui/core/Typography'; import CustomTextField from 'components/fields/CustomTextField'; -import MultiAutocompleteFieldVariableSize from 'components/fields/MultiAutocompleteFieldVariableSize'; import { useFormikContext } from 'formik'; +import { IIDIRAccessRequestDataObject } from 'interfaces/useAdminApi.interface'; import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface'; import React from 'react'; import yup from 'utils/YupSchema'; -export interface IIDIRRequestForm { - role: number; - work_from_regional_office: string; - regional_offices: number[]; - comments: string; -} - -export const IDIRRequestFormInitialValues: IIDIRRequestForm = { +export const IDIRRequestFormInitialValues: IIDIRAccessRequestDataObject = { role: ('' as unknown) as number, - work_from_regional_office: '', - regional_offices: [], - comments: '' + reason: '' }; export const IDIRRequestFormYupSchema = yup.object().shape({ role: yup.string().required('Required'), - // work_from_regional_office: yup.string().required('Required'), // TODO Release 1 Patch (BHBC-1442): Remove field validation - // regional_offices: yup - // .array() - // .when('work_from_regional_office', { is: 'true', then: yup.array().min(1, 'Required').required('Required') }), // TODO Release 1 Patch (BHBC-1442): Remove field validation - comments: yup.string().max(300, 'Maximum 300 characters') + reason: yup.string().max(300, 'Maximum 300 characters') }); export interface IIDIRRequestFormProps { codes?: IGetAllCodeSetsResponse; } -const useStyles = makeStyles((theme: Theme) => ({ - legend: { - marginTop: '1rem', - float: 'left', - marginBottom: '0.75rem', - letterSpacing: '-0.01rem' - } -})); - /** * Access Request - IDIR request fields * * @return {*} */ const IDIRRequestForm: React.FC = (props) => { - const classes = useStyles(); - const { values, touched, errors, setFieldValue, handleChange } = useFormikContext(); + const { values, touched, errors, handleChange } = useFormikContext(); const { codes } = props; return ( -

Select which role you want to be assigned to

- - Role - + {codes?.system_roles.map((item) => ( {item.name} ))} - - {errors.role} - -
- - {false && ( // TODO Release 1 Patch (BHBC-1442): Remove regional offices section - - { - if (event.target.value === 'false') { - setFieldValue('regional_offices', []); - } - }} - error={touched.work_from_regional_office && Boolean(errors.work_from_regional_office)}> - - Do you work for a Regional Office? - - - - } - label="Yes" - /> - } - label="No" - /> - {errors.work_from_regional_office} - - + + {errors.role} - - )} - - {values.work_from_regional_office === 'true' && ( - -

Which Regional Offices do you work for?

- { - return { value: item.id, label: item.name }; - }) || [] - } - /> -
- )} - - -

Additional comments

- +
+ + + + Why are you requesting access to Species Inventory Management System (SIMS)? + + + + +
); }; diff --git a/app/src/pages/access/__snapshots__/AccessRequestPage.test.tsx.snap b/app/src/pages/access/__snapshots__/AccessRequestPage.test.tsx.snap index 81b5999850..26c3a363f6 100644 --- a/app/src/pages/access/__snapshots__/AccessRequestPage.test.tsx.snap +++ b/app/src/pages/access/__snapshots__/AccessRequestPage.test.tsx.snap @@ -3,44 +3,49 @@ exports[`AccessRequestPage renders correctly 1`] = `
-

- Request Access to SIMS -

-
- You will need to provide some additional details before accessing this application. Complete the form below to request access. -
+ Request Access +
-

- Request Details -

-
-
+ You will need to provide some additional details before accessing this application. +

+
+
+ +
+

+ What role do you want to be assigned? +

-

- Select which role you want to be assigned to -

+
+
+
+

+ Why are you requesting access to Species Inventory Management System (SIMS)? +

+
-

- Additional comments -

+
- -
+