From c5e78654a6b967c8537a55600fce812511fc1c8b Mon Sep 17 00:00:00 2001 From: One Community Date: Wed, 14 Aug 2024 20:41:15 -0700 Subject: [PATCH 01/11] Revert "Revert "Revert "Revert "Backend Release to Main [1.92]"""" --- README.md | 2 - package-lock.json | 8370 +++++++++-------- package.json | 6 +- .../dashBoardController/dashboarddata.md | 12 - .../editSuggestionOption.md | 17 - .../dashBoardController/getAIPrompt.md | 17 - .../getPromptCopiedDate.md | 14 - .../getSuggestionOption.md | 15 - .../dashBoardController/leaderboarddata.md | 15 - .../dashBoardController/monthlydata.md | 14 - requirements/dashBoardController/orgData.md | 14 - .../dashBoardController/sendBugReport.md | 14 - .../dashBoardController/sendMakeSuggestion.md | 14 - .../dashBoardController/updateAIPrompt.md | 16 - .../dashBoardController/updateCopiedPrompt.md | 16 - .../dashBoardController/weeklydata.md | 13 - requirements/forcePwdController/forcePwd.md | 14 - .../forgotPwdController/postForgotPwd.md | 16 - .../inventoryController/getAllInvInProject.md | 17 + .../getAllInvInProjectWBS.md | 17 + .../postInvInProjectWBS.md | 19 + .../logincontroller/getUser-usecase.md | 9 - requirements/logincontroller/login-usecase.md | 21 - .../mapLocationsController/deleteLocation.md | 17 + .../mapLocationsController/getAllLocations.md | 17 + .../mapLocationsController/putUserLocation.md | 17 + .../updateUserLocation.md | 19 + .../createMouseoverText.md | 11 - .../getMouseoverText.md | 11 - .../updateMouseoverText.md | 12 - .../creatUserNotification.md | 15 - .../deleteUserNotification.md | 14 - .../getSentNotifications.md | 14 - .../getUnreadUserNotifications.md | 15 - .../getUserNotifications.md | 15 - .../markNotificationAsRead.md | 14 - .../deleteOwnerMessage.md | 13 - .../ownerMessageController/getOwnerMessage.md | 14 - .../updateOwnerMessage.md | 13 - .../rolePresetsController/createNewPresets.md | 18 - .../rolePresetsController/deletePresetById.md | 17 - .../rolePresetsController/getPresetsByRole.md | 15 - .../rolePresetsController/updatePresetById.md | 17 - requirements/rolesController/createNewRole.md | 24 - .../rolesController/deleteRoleById.md | 15 - requirements/rolesController/getAllRoles.md | 11 - requirements/rolesController/getRolesById.md | 14 - .../rolesController/updateRoleById.md | 28 - .../deleteTimeOffRequestById.md | 21 - .../getTimeOffRequestById.md | 15 - .../getTimeOffRequests.md | 16 - .../setTimeOffRequest.md | 20 - .../updateTimeOffRequestById.md | 26 - src/app.js | 11 +- .../BlueSquareEmailAssignmentController.js | 67 - src/controllers/badgeController.js | 91 +- src/controllers/badgeController.spec.js | 518 +- .../bmdashboard/bmEquipmentController.js | 40 - .../bmdashboard/bmInventoryTypeController.js | 116 +- .../bmdashboard/bmReusableController.js | 153 +- .../bmdashboard/bmToolController.js | 141 +- src/controllers/dashBoardController.js | 4 +- src/controllers/dashBoardController.spec.js | 838 -- src/controllers/forcePwdController.spec.js | 68 - src/controllers/forgotPwdcontroller.spec.js | 183 - src/controllers/inventoryController.js | 6 +- src/controllers/inventoryController.spec.js | 334 + src/controllers/logincontroller.js | 66 +- src/controllers/logincontroller.spec.js | 211 - src/controllers/mapLocationsController.js | 41 +- .../mapLocationsController.spec.js | 367 + src/controllers/mouseoverTextController.js | 78 +- .../mouseoverTextController.spec.js | 162 - src/controllers/notificationController.js | 60 +- .../notificationController.spec.js | 313 - .../ownerMessageController.spec.js | 139 - .../profileInitialSetupController.js | 318 +- src/controllers/projectController.js | 282 +- src/controllers/reportsController.js | 285 +- src/controllers/rolePresetsController.js | 43 +- src/controllers/rolePresetsController.spec.js | 444 - src/controllers/rolesController.js | 98 +- src/controllers/rolesController.spec.js | 229 - src/controllers/taskController.js | 128 +- .../taskEditSuggestionController.js | 32 +- src/controllers/teamController.js | 82 - src/controllers/timeEntryController.js | 567 +- .../timeOffRequestController.spec.js | 1260 --- src/controllers/titleController.js | 14 +- src/controllers/userProfileController.js | 458 +- src/controllers/wbsController.js | 55 +- src/cronjobs/userProfileJobs.js | 1 + src/helpers/dashboardhelper.js | 276 +- src/helpers/helperModels/userProjects.js | 17 + src/helpers/overviewReportHelper.js | 645 -- src/helpers/overviewReportHelper.spec.js | 64 - src/helpers/taskHelper.js | 333 +- src/helpers/userHelper.js | 575 +- src/models/BlueSquareEmailAssignment.js | 10 - .../bmdashboard/buildingInventoryItem.js | 13 +- .../bmdashboard/buildingInventoryType.js | 22 +- src/models/project.js | 18 +- src/models/team.js | 12 +- src/models/timeentry.js | 1 - src/models/userProfile.js | 32 +- src/models/wbs.js | 2 + src/routes/BlueSquareEmailAssignmentRouter.js | 18 - src/routes/badgeRouter.js | 19 +- src/routes/bmdashboard/bmEquipmentRouter.js | 2 - .../bmdashboard/bmInventoryTypeRouter.js | 4 - src/routes/bmdashboard/bmReusableRouter.js | 6 - src/routes/bmdashboard/bmToolRouter.js | 10 +- src/routes/forgotPwdRouter.test.js | 83 - src/routes/mapLocationsRouter.test.js | 200 + src/routes/mouseoverTextRouter.test.js | 98 - src/routes/reportsRouter.js | 30 +- src/routes/rolePresetRouter.test.js | 238 - src/routes/taskRouter.js | 50 +- src/routes/teamRouter.js | 14 +- src/routes/timeentryRouter.js | 2 - src/routes/userProfileRouter.js | 42 +- src/routes/wbsRouter.js | 10 +- src/server.js | 4 +- src/services/userService.js | 37 - src/startup/logger.js | 25 +- src/startup/routes.js | 11 +- src/test/assertions.js | 1 - src/test/createTestPermissions.js | 15 +- src/test/db/createUser.js | 17 +- src/test/mock-response.js | 1 - src/utilities/addMembersToTeams.js | 17 +- src/utilities/constants.js | 14 - src/utilities/createInitialPermissions.js | 20 +- src/utilities/emailSender.js | 14 +- src/utilities/errorHandling/customError.js | 48 - .../errorHandling/globalErrorHandler.js | 55 - src/utilities/exceptionHandler.js | 17 + src/utilities/htmlContentSanitizer.js | 4 +- src/utilities/nodeCache.js | 10 - src/utilities/objectUtils.js | 60 - src/utilities/permission.spec.js | 84 - src/utilities/permissions.js | 108 +- src/utilities/timeUtils.js | 2 +- src/websockets/TimerService/clientsHandler.js | 54 +- src/websockets/index.js | 99 +- 145 files changed, 7101 insertions(+), 13935 deletions(-) delete mode 100644 requirements/dashBoardController/dashboarddata.md delete mode 100644 requirements/dashBoardController/editSuggestionOption.md delete mode 100644 requirements/dashBoardController/getAIPrompt.md delete mode 100644 requirements/dashBoardController/getPromptCopiedDate.md delete mode 100644 requirements/dashBoardController/getSuggestionOption.md delete mode 100644 requirements/dashBoardController/leaderboarddata.md delete mode 100644 requirements/dashBoardController/monthlydata.md delete mode 100644 requirements/dashBoardController/orgData.md delete mode 100644 requirements/dashBoardController/sendBugReport.md delete mode 100644 requirements/dashBoardController/sendMakeSuggestion.md delete mode 100644 requirements/dashBoardController/updateAIPrompt.md delete mode 100644 requirements/dashBoardController/updateCopiedPrompt.md delete mode 100644 requirements/dashBoardController/weeklydata.md delete mode 100644 requirements/forcePwdController/forcePwd.md delete mode 100644 requirements/forgotPwdController/postForgotPwd.md create mode 100644 requirements/inventoryController/getAllInvInProject.md create mode 100644 requirements/inventoryController/getAllInvInProjectWBS.md create mode 100644 requirements/inventoryController/postInvInProjectWBS.md delete mode 100644 requirements/logincontroller/getUser-usecase.md delete mode 100644 requirements/logincontroller/login-usecase.md create mode 100644 requirements/mapLocationsController/deleteLocation.md create mode 100644 requirements/mapLocationsController/getAllLocations.md create mode 100644 requirements/mapLocationsController/putUserLocation.md create mode 100644 requirements/mapLocationsController/updateUserLocation.md delete mode 100644 requirements/mouseoverTextController/createMouseoverText.md delete mode 100644 requirements/mouseoverTextController/getMouseoverText.md delete mode 100644 requirements/mouseoverTextController/updateMouseoverText.md delete mode 100644 requirements/notificationController/creatUserNotification.md delete mode 100644 requirements/notificationController/deleteUserNotification.md delete mode 100644 requirements/notificationController/getSentNotifications.md delete mode 100644 requirements/notificationController/getUnreadUserNotifications.md delete mode 100644 requirements/notificationController/getUserNotifications.md delete mode 100644 requirements/notificationController/markNotificationAsRead.md delete mode 100644 requirements/ownerMessageController/deleteOwnerMessage.md delete mode 100644 requirements/ownerMessageController/getOwnerMessage.md delete mode 100644 requirements/ownerMessageController/updateOwnerMessage.md delete mode 100644 requirements/rolePresetsController/createNewPresets.md delete mode 100644 requirements/rolePresetsController/deletePresetById.md delete mode 100644 requirements/rolePresetsController/getPresetsByRole.md delete mode 100644 requirements/rolePresetsController/updatePresetById.md delete mode 100644 requirements/rolesController/createNewRole.md delete mode 100644 requirements/rolesController/deleteRoleById.md delete mode 100644 requirements/rolesController/getAllRoles.md delete mode 100644 requirements/rolesController/getRolesById.md delete mode 100644 requirements/rolesController/updateRoleById.md delete mode 100644 requirements/timeOffRequestController/deleteTimeOffRequestById.md delete mode 100644 requirements/timeOffRequestController/getTimeOffRequestById.md delete mode 100644 requirements/timeOffRequestController/getTimeOffRequests.md delete mode 100644 requirements/timeOffRequestController/setTimeOffRequest.md delete mode 100644 requirements/timeOffRequestController/updateTimeOffRequestById.md delete mode 100644 src/controllers/BlueSquareEmailAssignmentController.js delete mode 100644 src/controllers/dashBoardController.spec.js delete mode 100644 src/controllers/forcePwdController.spec.js delete mode 100644 src/controllers/forgotPwdcontroller.spec.js create mode 100644 src/controllers/inventoryController.spec.js delete mode 100644 src/controllers/logincontroller.spec.js create mode 100644 src/controllers/mapLocationsController.spec.js delete mode 100644 src/controllers/mouseoverTextController.spec.js delete mode 100644 src/controllers/notificationController.spec.js delete mode 100644 src/controllers/ownerMessageController.spec.js delete mode 100644 src/controllers/rolePresetsController.spec.js delete mode 100644 src/controllers/rolesController.spec.js delete mode 100644 src/controllers/timeOffRequestController.spec.js create mode 100644 src/helpers/helperModels/userProjects.js delete mode 100644 src/helpers/overviewReportHelper.js delete mode 100644 src/helpers/overviewReportHelper.spec.js delete mode 100644 src/models/BlueSquareEmailAssignment.js delete mode 100644 src/routes/BlueSquareEmailAssignmentRouter.js delete mode 100644 src/routes/forgotPwdRouter.test.js create mode 100644 src/routes/mapLocationsRouter.test.js delete mode 100644 src/routes/mouseoverTextRouter.test.js delete mode 100644 src/routes/rolePresetRouter.test.js delete mode 100644 src/services/userService.js delete mode 100644 src/utilities/constants.js delete mode 100644 src/utilities/errorHandling/customError.js delete mode 100644 src/utilities/errorHandling/globalErrorHandler.js create mode 100644 src/utilities/exceptionHandler.js delete mode 100644 src/utilities/objectUtils.js delete mode 100644 src/utilities/permission.spec.js diff --git a/README.md b/README.md index bbea44b69..90890f72a 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,8 @@ SMTPPort= SMTPUser= TOKEN_LIFETIME= TOKEN_LIFETIME_UNITS= -NODE_ENV= `local` | `development` | `production`
JWT_SECRET= - To make the process easy create a .env file and put the above text in the file and replace values with the correct values, which you can get from your teammates. Then do an npm run-script build followed by an npm start. By default, the services will start on port 4500 and you can http://localhost:4500/api/ to access the methods. A tools like Postman will be your best friend here, you will need to have an auth token placed in the 'Authorization' header which you can get through the networking tab of the local frontend when you login. - `npm run lint` -- fix lint diff --git a/package-lock.json b/package-lock.json index 3c4e3e99e..fd52a1746 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,9 +71,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -89,9 +89,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } @@ -135,9 +135,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -180,9 +180,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -609,23 +609,6 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, - "@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "dev": true - } - } - }, "@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", @@ -690,23 +673,6 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, - "@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "dev": true - } - } - }, "@babel/plugin-transform-arrow-functions": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", @@ -945,9 +911,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -1091,9 +1057,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -1282,6 +1248,16 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1449,42 +1425,19 @@ "dev": true }, "@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", "dev": true, "requires": { - "@jest/types": "^29.6.3", + "@jest/types": "^26.6.2", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", "slash": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1543,64 +1496,41 @@ } }, "@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", "dev": true, "requires": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "ci-info": "^3.2.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1641,31 +1571,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -1684,40 +1589,75 @@ } }, "@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", "dev": true, "requires": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", "@types/node": "*", - "jest-mock": "^29.7.0" + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1758,6 +1698,18 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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 + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1769,70 +1721,82 @@ } } }, - "@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "requires": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "@sinclair/typebox": "^0.27.8" } }, - "@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", "dev": true, "requires": { - "jest-get-type": "^29.6.3" + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" }, "dependencies": { - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "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 } } }, - "@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", "dev": true, "requires": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + } + }, + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1873,6 +1837,18 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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 + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1884,41 +1860,19 @@ } } }, - "@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1970,1112 +1924,699 @@ } } }, - "@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" }, "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "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==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + } } } } }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, + "@jridgewell/resolve-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", + "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", + "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", "requires": { - "@sinclair/typebox": "^0.27.8" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "dependencies": { - "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - } + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@redis/bloom": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", + "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==" + }, + "@redis/client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", + "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", + "requires": { + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "yallist": "4.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "@redis/graph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", + "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==" + }, + "@redis/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", + "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==" + }, + "@redis/search": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", + "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==" + }, + "@redis/time-series": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", + "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==" + }, + "@sentry-internal/tracing": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.0.tgz", + "integrity": "sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==", + "requires": { + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" + } + }, + "@sentry/core": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.0.tgz", + "integrity": "sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==", + "requires": { + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" + } + }, + "@sentry/integrations": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.110.0.tgz", + "integrity": "sha512-cWpEGMTyX1XO4jb0NXMh1thkkiSajM5ydE/ceAdxmG9V7gv7E1pREK8P1NeVvzvjZ67z+uVWYbgYwXxd4eqZ/A==", + "requires": { + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0", + "localforage": "^1.8.1" + } + }, + "@sentry/node": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.110.0.tgz", + "integrity": "sha512-YPfweCSzo/omnx5q1xOEZfI8Em3jnPqj7OM4ObXmoSKEK+kM1oUF3BTRzw5BJOaOCSTBFY1RAsGyfVIyrwxWnA==", + "requires": { + "@sentry-internal/tracing": "7.110.0", + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" + } + }, + "@sentry/types": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.0.tgz", + "integrity": "sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==" + }, + "@sentry/utils": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.0.tgz", + "integrity": "sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==", + "requires": { + "@sentry/types": "7.110.0" + } + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@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" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true }, - "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==", + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "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==", + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" } } } }, - "@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, "requires": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } + "@babel/types": "^7.0.0" } }, - "@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "requires": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "dev": true, + "requires": { + "@babel/types": "^7.20.7" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "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 - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "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==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" } } } }, - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - } + "@types/connect": "*", + "@types/node": "*" } }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "@types/bson": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", + "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "dependencies": { - "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - } - } - } + "@types/node": "*" } }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@types/node": "*" } }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "optional": true - }, - "@nodelib/fs.scandir": { + "@types/cookiejar": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true + }, + "@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==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@types/node": "*" } }, - "@redis/bloom": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", - "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==" + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true }, - "@redis/client": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", - "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, "requires": { - "cluster-key-slot": "1.1.0", - "generic-pool": "3.8.2", - "yallist": "4.0.0" - }, - "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } + "@types/istanbul-lib-coverage": "*" } }, - "@redis/graph": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", - "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==" + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } }, - "@redis/json": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", - "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==" + "@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } }, - "@redis/search": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", - "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==" + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true }, - "@redis/time-series": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", - "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==" + "@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true }, - "@sentry-internal/tracing": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.0.tgz", - "integrity": "sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==", + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", "requires": { - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" + "@types/bson": "*", + "@types/node": "*" } }, - "@sentry/core": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.0.tgz", - "integrity": "sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==", + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" + }, + "@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, + "@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==", + "dev": true + }, + "@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==", + "dev": true + }, + "@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==", + "dev": true, "requires": { - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" + "@types/mime": "^1", + "@types/node": "*" } }, - "@sentry/integrations": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.110.0.tgz", - "integrity": "sha512-cWpEGMTyX1XO4jb0NXMh1thkkiSajM5ydE/ceAdxmG9V7gv7E1pREK8P1NeVvzvjZ67z+uVWYbgYwXxd4eqZ/A==", + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/superagent": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", + "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", + "dev": true, "requires": { - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0", - "localforage": "^1.8.1" + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*" } }, - "@sentry/node": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.110.0.tgz", - "integrity": "sha512-YPfweCSzo/omnx5q1xOEZfI8Em3jnPqj7OM4ObXmoSKEK+kM1oUF3BTRzw5BJOaOCSTBFY1RAsGyfVIyrwxWnA==", + "@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, "requires": { - "@sentry-internal/tracing": "7.110.0", - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" } }, - "@sentry/types": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.0.tgz", - "integrity": "sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==" + "@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true }, - "@sentry/utils": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.0.tgz", - "integrity": "sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==", + "@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, "requires": { - "@sentry/types": "7.110.0" + "@types/yargs-parser": "*" } }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, - "@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "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 + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "requires": { - "type-detect": "4.0.8" + "event-target-shim": "^5.0.0" } }, - "@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { - "@sinonjs/commons": "^3.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" } }, - "@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" }, "dependencies": { - "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true - }, - "@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", - "dev": true - }, - "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } } } }, - "@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } + "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 }, - "@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true }, - "@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", - "dev": true, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "requires": { - "@babel/types": "^7.20.7" - }, - "dependencies": { - "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true - }, - "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - } - } + "debug": "4" } }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "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, "requires": { - "@types/connect": "*", - "@types/node": "*" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" } }, - "@types/bson": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", - "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "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": { - "@types/node": "*" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "requires": { - "@types/node": "*" + "type-fest": "^0.21.3" } }, - "@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "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 }, - "@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==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", - "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==", "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" + "color-convert": "^1.9.0" } }, - "@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "requires": { - "@types/node": "*" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, - "@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "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 }, - "@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "requires": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" + "dequal": "^2.0.3" } }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true - }, - "@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", "dev": true }, - "@types/mongodb": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", - "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", - "requires": { - "@types/bson": "*", - "@types/node": "*" - } - }, - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - }, - "@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==", + "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 }, - "@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==", + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true }, - "@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==", + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", "dev": true, "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "@types/superagent": { - "version": "8.1.6", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", - "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", - "dev": true, - "requires": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*" - } - }, - "@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, - "requires": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" - } - }, - "@types/tmp": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", - "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", - "dev": true - }, - "@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "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 - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" - } - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true - }, - "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 - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "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, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "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-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - } - }, - "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 - }, - "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==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "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 - }, - "aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "requires": { - "dequal": "^2.0.3" - } - }, - "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" } }, "array-flatten": { @@ -3251,6 +2792,12 @@ } } }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true + }, "array.prototype.findlastindex": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", @@ -4277,6 +3824,12 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -4302,6 +3855,12 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "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 + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -4673,9 +4232,9 @@ }, "dependencies": { "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -4731,6 +4290,42 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base": { + "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", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4807,23 +4402,19 @@ } }, "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "requires": { - "fill-range": "^7.1.1" - }, - "dependencies": { - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "requires": { - "to-regex-range": "^5.0.1" - } - } + "fill-range": "^7.0.1" } }, + "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 + }, "browserslist": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.0.tgz", @@ -4881,6 +4472,23 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "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, + "requires": { + "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" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4903,9 +4511,18 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001646", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz", - "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==" + "version": "1.0.30001576", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", + "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==" + }, + "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, + "requires": { + "rsvp": "^4.8.4" + } }, "chalk": { "version": "2.4.2", @@ -4939,17 +4556,40 @@ } }, "ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, "cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", "dev": true }, + "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, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -5023,36 +4663,73 @@ } }, "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" - }, - "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==", - "requires": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - } - }, - "cluster-key-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", - "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" - }, + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + }, + "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==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5065,6 +4742,16 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5161,6 +4848,12 @@ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true + }, "core-js": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", @@ -5196,95 +4889,6 @@ "vary": "^1" } }, - "create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "cron": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz", @@ -5321,12 +4925,74 @@ } } }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "dependencies": { + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + } + } + }, "data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -5470,10 +5136,22 @@ "ms": "2.1.2" } }, - "dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "deep-is": { @@ -5505,6 +5183,28 @@ "object-keys": "^1.0.12" } }, + "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, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5576,6 +5276,23 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, "domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -5624,9 +5341,9 @@ "integrity": "sha512-Gs7xVpIZ7tYYSDA+WgpzwpPvfGwUk3KSIjJ0akuj5XQHFdyQnsUoM76EA4CIHXNLPiVwTwOFay9RMb0ChG3OBw==" }, "emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", "dev": true }, "emoji-regex": { @@ -5790,6 +5507,33 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, + "escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "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 + } + } + }, "eslint": { "version": "8.47.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", @@ -6626,1181 +6370,625 @@ "eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true - }, - "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, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "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 - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - } - } - }, - "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 - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "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, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" - }, - "execa": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", - "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^3.0.1", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "dependencies": { - "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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true - }, - "mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true - }, - "onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "requires": { - "mimic-fn": "^4.0.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "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, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "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 - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "requires": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "dependencies": { - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - } - } - }, - "express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.19.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.2", - "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.7", - "qs": "6.9.7", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "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==" - } - } - }, - "express-validator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", - "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", - "requires": { - "lodash": "^4.17.21", - "validator": "^13.9.0" - }, - "dependencies": { - "validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "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==", - "dev": true - }, - "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 - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "fast-text-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", - "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "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, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "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" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - } - } - }, - "find-babel-config": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.0.0.tgz", - "integrity": "sha512-dOKT7jvF3hGzlW60Gc3ONox/0rRZ/tz7WCil0bqA1In/3I8f1BctpXahRnEKDySZqci7u+dqq93sZST9fOJpFw==", - "requires": { - "json5": "^2.1.1", - "path-exists": "^4.0.0" - }, - "dependencies": { - "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==" - } - } - }, - "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==", - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", - "dev": true - }, - "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, - "requires": { - "is-callable": "^1.1.3" - } - }, - "formidable": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", - "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", - "requires": { - "dezalgo": "^1.0.4", - "hexoid": "^1.0.0", - "once": "^1.4.0", - "qs": "^6.11.0" - }, - "dependencies": { - "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "requires": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - } - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" - }, - "qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", - "requires": { - "side-channel": "^1.0.6" - } - }, - "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "requires": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - } - } - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gaxios": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", - "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", - "requires": { - "abort-controller": "^3.0.0", - "extend": "^3.0.2", - "https-proxy-agent": "^5.0.0", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.7" - } - }, - "gcp-metadata": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", - "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", - "requires": { - "gaxios": "^4.0.0", - "json-bigint": "^1.0.0" - } - }, - "generic-pool": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", - "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" - }, - "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==" - }, - "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==", - "dev": true - }, - "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" - } - }, - "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 - }, - "get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "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" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "google-auth-library": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", - "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", - "requires": { - "arrify": "^2.0.0", - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "fast-text-encoding": "^1.0.0", - "gaxios": "^4.0.0", - "gcp-metadata": "^4.2.0", - "gtoken": "^5.0.4", - "jws": "^4.0.0", - "lru-cache": "^6.0.0" - }, - "dependencies": { - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "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==", - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } - }, - "google-p12-pem": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", - "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true + }, + "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, "requires": { - "node-forge": "^1.3.1" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "googleapis": { - "version": "100.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-100.0.0.tgz", - "integrity": "sha512-RToFQGY54B756IDbjdyjb1vWFmn03bYpXHB2lIf0eq2UBYsIbYOLZ0kqSomfJnpclEukwEmMF7Jn6Wsev871ew==", + "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 + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, "requires": { - "google-auth-library": "^7.0.2", - "googleapis-common": "^5.0.2" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + } } }, - "googleapis-common": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", - "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", + "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 + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "requires": { - "extend": "^3.0.2", - "gaxios": "^4.0.0", - "google-auth-library": "^7.14.0", - "qs": "^6.7.0", - "url-template": "^2.0.8", - "uuid": "^8.0.0" + "estraverse": "^5.1.0" }, "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } } }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "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, "requires": { - "get-intrinsic": "^1.1.3" + "estraverse": "^5.2.0" }, "dependencies": { - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - } + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true } } }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "exec-sh": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", + "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", "dev": true }, - "gtoken": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", - "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, "requires": { - "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", - "jws": "^4.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" }, "dependencies": { - "jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "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, "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "jws": { + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, "requires": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "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, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "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 + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" } } } }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, "requires": { - "function-bind": "^1.1.2" + "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" }, "dependencies": { - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + "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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true } } }, - "hexoid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", - "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "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 - }, - "htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "expect": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "dev": true, "requires": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" + "@jest/types": "^26.6.2", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } } }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", "depd": "~1.1.2", - "inherits": "2.0.4", + "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.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "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==" + } } }, - "human-signals": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", - "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", - "dev": true - }, - "husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "express-validator": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.0.1.tgz", + "integrity": "sha512-oB+z9QOzQIE8FnlINqyIFA8eIckahC6qc8KtqLdLJcU3/phVyuhXH3bA4qzcrhme+1RYaCSwrq+TlZ/kAKIARA==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "lodash": "^4.17.21", + "validator": "^13.9.0" + }, + "dependencies": { + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + } } }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "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, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "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, "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "is-plain-object": "^2.0.4" } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + } + } + }, + "extglob": { + "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", + "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" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, "requires": { - "p-locate": "^4.1.0" + "is-descriptor": "^1.0.0" } }, - "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==", + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "requires": { - "p-limit": "^2.2.0" + "is-extendable": "^0.1.0" } }, - "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 - }, - "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==", + "is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, "requires": { - "find-up": "^4.0.0" + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" } } } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "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==", "dev": true }, - "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==", + "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 }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, - "internal-slot": { + "fast-text-encoding": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "reusify": "^1.0.4" } }, - "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==" + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } }, - "is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } }, - "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "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, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "flat-cache": "^3.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==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "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" }, "dependencies": { - "get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dev": true, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "ms": "2.0.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "find-babel-config": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.0.0.tgz", + "integrity": "sha512-dOKT7jvF3hGzlW60Gc3ONox/0rRZ/tz7WCil0bqA1In/3I8f1BctpXahRnEKDySZqci7u+dqq93sZST9fOJpFw==", "requires": { - "has-bigints": "^1.0.1" + "json5": "^2.1.1", + "path-exists": "^4.0.0" + }, + "dependencies": { + "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==" + } } }, - "is-binary-path": { + "find-cache-dir": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "requires": { - "binary-extensions": "^2.0.0" + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" } }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "locate-path": "^3.0.0" } }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.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==", + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "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, "requires": { - "has": "^1.0.3" + "is-callable": "^1.1.3" } }, - "is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, "requires": { - "is-typed-array": "^1.1.13" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formidable": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", + "integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==", + "requires": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0", + "qs": "^6.11.0" }, "dependencies": { - "available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "requires": { - "possible-typed-array-names": "^1.0.0" - } - }, "call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -7811,15 +6999,13 @@ }, "function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -7828,899 +7014,1142 @@ "hasown": "^2.0.0" } }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.3" - } + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" }, - "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, + "qs": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", + "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", "requires": { - "which-typed-array": "^1.1.14" + "side-channel": "^1.0.6" } }, - "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } } } }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, "requires": { - "has-tostringtag": "^1.0.0" + "map-cache": "^0.2.2" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, - "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==", + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, - "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 + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==" }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, "requires": { - "is-extglob": "^2.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" } }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "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==" + "gaxios": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.3.tgz", + "integrity": "sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "gcp-metadata": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", "requires": { - "has-tostringtag": "^1.0.0" + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" } }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" + }, + "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==" + }, + "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==", "dev": true }, - "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==", + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "requires": { - "isobject": "^3.0.1" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" } }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "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 + }, + "get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "requires": { "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.1.1" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "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" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "google-auth-library": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.14.1.tgz", + "integrity": "sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "google-p12-pem": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.4.tgz", + "integrity": "sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg==", "requires": { - "has-tostringtag": "^1.0.0" + "node-forge": "^1.3.1" } }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "googleapis": { + "version": "100.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-100.0.0.tgz", + "integrity": "sha512-RToFQGY54B756IDbjdyjb1vWFmn03bYpXHB2lIf0eq2UBYsIbYOLZ0kqSomfJnpclEukwEmMF7Jn6Wsev871ew==", "requires": { - "has-symbols": "^1.0.2" + "google-auth-library": "^7.0.2", + "googleapis-common": "^5.0.2" } }, - "is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", - "dev": true, + "googleapis-common": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.1.0.tgz", + "integrity": "sha512-RXrif+Gzhq1QAzfjxulbGvAY3FPj8zq/CYcvgjzDbaBNCD6bUl+86I7mUs4DKWHGruuK26ijjR/eDpWIDgNROA==", "requires": { - "which-typed-array": "^1.1.11" + "extend": "^3.0.2", + "gaxios": "^4.0.0", + "google-auth-library": "^7.14.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } } }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "requires": { - "call-bind": "^1.0.2" + "get-intrinsic": "^1.1.3" + }, + "dependencies": { + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + } } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" - }, - "istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", "dev": true, + "optional": true + }, + "gtoken": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", + "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", "requires": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "gaxios": "^4.0.0", + "google-p12-pem": "^3.1.3", + "jws": "^4.0.0" }, "dependencies": { - "@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - } - }, - "@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", - "dev": true - }, - "@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "@babel/helper-environment-visitor": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-function-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, - "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, - "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - } - }, - "@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "requires": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, - "requires": { - "@babel/types": "^7.24.7" - } - }, - "@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", - "dev": true - }, - "@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dev": true, - "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - } - }, - "@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true - }, - "@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - } - }, - "@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", "requires": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "kind-of": "^3.0.2" }, "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } } } }, - "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - } - }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.815", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", - "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", - "dev": true - }, - "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==", + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", "dev": true, "requires": { - "yallist": "^3.0.2" + "is-buffer": "^1.1.5" } - }, - "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true - }, - "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true } } }, - "istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "function-bind": "^1.1.2" }, "dependencies": { - "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 - }, - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "requires": { - "semver": "^7.5.3" - } - }, - "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "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==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" } } }, - "istanbul-lib-source-maps": { + "hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==" + }, + "homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "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 + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "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 + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "http-proxy-agent": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "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 - } + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" } }, - "istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "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, "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, - "jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "requires": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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, "requires": { - "color-convert": "^2.0.1" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "p-locate": "^4.1.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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, "requires": { - "color-name": "~1.1.4" + "p-limit": "^2.2.0" } }, - "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 - }, - "has-flag": { + "path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "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, "requires": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" + "find-up": "^4.0.0" } - }, - "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==", + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "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 + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "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==" + }, + "is": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", + "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" + }, + "is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "dependencies": { + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" } } } }, - "jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "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==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "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 + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + }, + "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, + "requires": { + "ci-info": "^2.0.0" + } + }, + "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" + } + }, + "is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", "dev": true, "requires": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" + "is-typed-array": "^1.1.13" }, "dependencies": { - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "possible-typed-array-names": "^1.0.0" } }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "requires": { - "path-key": "^3.0.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "requires": { - "yocto-queue": "^0.1.0" + "has-symbols": "^1.0.3" } }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + } + } + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "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 + }, + "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 + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "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==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.11" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "requires": { + "call-bind": "^1.0.2" + } + }, + "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 + }, + "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, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "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, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, - "jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "requires": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "yocto-queue": "^0.1.0" + "semver": "^7.5.3" } }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "lru-cache": "^6.0.0" } }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8732,59 +8161,46 @@ } } }, - "jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "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 + } + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", + "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "dev": true, + "requires": { + "@jest/core": "^26.6.3", + "import-local": "^3.0.2", + "jest-cli": "^26.6.3" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8825,43 +8241,27 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "jest-cli": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", "dev": true, "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "prompts": "^2.0.1", + "yargs": "^15.4.1" } }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8873,121 +8273,137 @@ } } }, - "jest-diff": { + "jest-changed-files": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", "dev": true, "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "@jest/types": "^26.6.2", + "execa": "^4.0.0", + "throat": "^5.0.0" }, "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "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, "requires": { - "color-name": "~1.1.4" + "pump": "^3.0.0" } }, - "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 - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "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==", + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "path-key": "^3.0.0" } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true } } }, - "jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } - }, - "jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "jest-config": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", "dev": true, "requires": { - "@jest/types": "^29.6.3", + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "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, "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "color-convert": "^2.0.1" } }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", "dev": true, "requires": { - "@types/yargs-parser": "*" + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" } }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" } }, "chalk": { @@ -9021,35 +8437,10 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "supports-color": { @@ -9063,43 +8454,18 @@ } } }, - "jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "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, "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9151,55 +8517,28 @@ } } }, - "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 + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } }, - "jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "jest-each": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", "dev": true, "requires": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.3.2", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9251,57 +8590,87 @@ } } }, - "jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", "dev": true, "requires": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - } + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" } }, - "jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "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 + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", "dev": true, "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" }, "dependencies": { "ansi-styles": { @@ -9338,61 +8707,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - } - }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9404,46 +8724,28 @@ } } }, - "jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9484,37 +8786,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9526,40 +8797,23 @@ } } }, - "jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", "dev": true, "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9600,6 +8854,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9611,6 +8871,16 @@ } } }, + "jest-mock": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + } + }, "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -9618,25 +8888,24 @@ "dev": true }, "jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", "dev": true }, "jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", "dev": true, "requires": { + "@jest/types": "^26.6.2", "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", + "graceful-fs": "^4.2.4", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", + "jest-util": "^26.6.2", + "read-pkg-up": "^7.0.1", + "resolve": "^1.18.1", "slash": "^3.0.0" }, "dependencies": { @@ -9698,67 +8967,44 @@ } }, "jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", "dev": true, "requires": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" } }, "jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", "dev": true, "requires": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" + "emittery": "^0.7.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9799,31 +9045,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "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, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "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 - }, - "source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9836,58 +9057,40 @@ } }, "jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/yargs": "^15.0.0", "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", + "cjs-module-lexer": "^0.6.0", "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", + "graceful-fs": "^4.2.4", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "strip-bom": "^4.0.0", + "yargs": "^15.4.1" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9951,57 +9154,40 @@ } } }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, "jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", "dev": true, "requires": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", + "@babel/types": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", + "expect": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" + "pretty-format": "^26.6.2", + "semver": "^7.3.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -10036,67 +9222,21 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - } - }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } + "lru-cache": "^6.0.0" } }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10108,43 +9248,20 @@ } } }, - "jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "requires": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -10197,42 +9314,19 @@ } }, "jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", "dev": true, "requires": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", + "jest-get-type": "^26.3.0", "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "pretty-format": "^26.6.2" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -10279,37 +9373,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true - }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10322,44 +9385,20 @@ } }, "jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", "dev": true, "requires": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", + "jest-util": "^26.6.2", "string-length": "^4.0.1" }, "dependencies": { - "@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - } - }, - "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -10412,15 +9451,14 @@ } }, "jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.7.0", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^7.0.0" }, "dependencies": { "has-flag": { @@ -10430,9 +9468,9 @@ "dev": true }, "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -10454,6 +9492,75 @@ "argparse": "^2.0.1" } }, + "jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true + } + } + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -10510,9 +9617,9 @@ } }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "requires": { "lru-cache": "^6.0.0" } @@ -11027,6 +10134,21 @@ "tmpl": "1.0.5" } }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, "md5-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", @@ -11108,6 +10230,27 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, + "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" + }, + "dependencies": { + "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, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -11382,6 +10525,25 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, + "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", + "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" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -11414,6 +10576,12 @@ } } }, + "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-cache": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", @@ -11455,6 +10623,40 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node-notifier": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", + "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", + "shellwords": "^0.1.1", + "uuid": "^8.3.0", + "which": "^2.0.2" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + } + } + }, "node-releases": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", @@ -11527,6 +10729,18 @@ "abbrev": "1" } }, + "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, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11549,11 +10763,48 @@ } } }, + "nwsapi": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.8.tgz", + "integrity": "sha512-GU/I3lTEFQ9mkEm07Q7HvdRajss8E1wVMGOk3/lHl60QPseG+B3BIQY+JUjYWw7gF8cCeoQCXd4N7DB7avw0Rg==", + "dev": true + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -11564,6 +10815,15 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, "object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", @@ -12094,6 +11354,15 @@ } } }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -12307,6 +11576,18 @@ "type-check": "^0.4.0" } }, + "p-each-series": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -12368,11 +11649,23 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -12447,6 +11740,12 @@ "find-up": "^3.0.0" } }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true + }, "possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -12556,29 +11855,45 @@ "ipaddr.js": "1.9.1" } }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "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 }, + "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, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, - "pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true - }, "qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12601,12 +11916,85 @@ "unpipe": "1.0.0" } }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "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, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "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, + "requires": { + "p-locate": "^4.1.0" + } + }, + "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, + "requires": { + "p-limit": "^2.2.0" + } + }, + "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 + }, + "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 + } + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -12668,6 +12056,16 @@ "@babel/runtime": "^7.8.4" } }, + "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, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, "regexp-clone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", @@ -12729,6 +12127,24 @@ } } }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, "require-at": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", @@ -12740,6 +12156,18 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, + "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==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "reselect": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", @@ -12778,10 +12206,16 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "dev": true + }, + "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 }, "reusify": { @@ -12805,6 +12239,12 @@ "glob": "^7.1.3" } }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12851,6 +12291,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, "safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -12881,6 +12330,214 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@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" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "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", + "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" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "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, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "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" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "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, + "requires": { + "pump": "^3.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "micromatch": { + "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", + "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" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "which": { + "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" + } + } + } + }, "sanitize-html": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", @@ -12915,10 +12572,19 @@ "sparse-bitfield": "^3.0.3" } }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "send": { "version": "0.17.2", @@ -12973,6 +12639,12 @@ "send": "0.17.2" } }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -13036,6 +12708,29 @@ } } }, + "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, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -13049,6 +12744,28 @@ "kind-of": "^6.0.2" } }, + "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, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "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 + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -13094,33 +12811,136 @@ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { - "lru-cache": "^6.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 + } + } + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" + }, + "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", + "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" + }, + "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" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "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, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + } + } + } + }, + "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, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" - }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13131,6 +12951,19 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, + "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", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -13147,6 +12980,12 @@ } } }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "dev": true + }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -13156,6 +12995,47 @@ "memory-pager": "^1.0.2" } }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "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, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dev": true + }, + "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, + "requires": { + "extend-shallow": "^3.0.0" + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13179,6 +13059,27 @@ } } }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -13903,6 +13804,12 @@ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, "strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -14035,11 +13942,44 @@ "has-flag": "^3.0.0" } }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "has-flag": "^4.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==" }, + "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 + }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -14077,6 +14017,16 @@ } } }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14094,10 +14044,16 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, "tmp": { @@ -14117,6 +14073,38 @@ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "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, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -14139,6 +14127,18 @@ "nopt": "~1.0.10" } }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14264,6 +14264,15 @@ "is-typed-array": "^1.1.9" } }, + "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, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -14305,31 +14314,65 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==" }, + "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", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, - "update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", "dev": true, "requires": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, "dependencies": { - "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } }, - "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", "dev": true } } @@ -14343,11 +14386,33 @@ "punycode": "^2.1.0" } }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "dev": true + }, + "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, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, + "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 + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14364,42 +14429,20 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz", + "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==", "dev": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" }, "dependencies": { - "@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true } } @@ -14412,6 +14455,16 @@ "homedir-polyfill": "^1.0.1" } }, + "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, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "validator": { "version": "10.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", @@ -14422,6 +14475,24 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, + "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, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -14436,6 +14507,21 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "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, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "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 + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -14466,6 +14552,12 @@ "is-symbol": "^1.0.3" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, "which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -14560,24 +14652,38 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "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, "requires": { "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==" + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==" + }, + "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 + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true }, "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { @@ -14592,25 +14698,69 @@ "dev": true }, "yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.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" + }, + "dependencies": { + "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, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "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, + "requires": { + "p-locate": "^4.1.0" + } + }, + "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, + "requires": { + "p-limit": "^2.2.0" + } + }, + "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 + } } }, "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } }, "yauzl": { "version": "2.10.0", diff --git a/package.json b/package.json index 8b88744dc..86c0e945d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint-import-resolver-babel-module": "^5.3.1", "eslint-plugin-import": "^2.28.0", "husky": "^8.0.1", - "jest": "^29.7.0", + "jest": "^26.6.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -69,7 +69,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.35", "mongodb": "^3.7.3", - "mongoose": "^5.13.20", + "mongoose": "^5.13.15", "mongoose-validator": "^2.1.0", "node-cache": "^5.1.2", "node-datetime": "^2.0.3", @@ -78,7 +78,7 @@ "sanitize-html": "^2.13.0", "supertest": "^6.3.4", "uuid": "^3.4.0", - "ws": "^8.17.1" + "ws": "^8.8.1" }, "nodemonConfig": { "watch": [ diff --git a/requirements/dashBoardController/dashboarddata.md b/requirements/dashBoardController/dashboarddata.md deleted file mode 100644 index d2ec6a3b5..000000000 --- a/requirements/dashBoardController/dashboarddata.md +++ /dev/null @@ -1,12 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if there is no error and return results - -> ## Negative case - -> ## Edge case diff --git a/requirements/dashBoardController/editSuggestionOption.md b/requirements/dashBoardController/editSuggestionOption.md deleted file mode 100644 index 5a0c1acbc..000000000 --- a/requirements/dashBoardController/editSuggestionOption.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if suggestionData.field is added a new field -2. ✅ Returns 200 if suggestionData.suggestion is added a new suggestion -3. ✅ Returns 200 if suggestionData.field is deleted -4. ✅ Returns 200 if suggestionData.suggestion is deleted - -> ## Negative case - -1. ❌ Returns 500 if there is an error in the function - -> ## Edge case diff --git a/requirements/dashBoardController/getAIPrompt.md b/requirements/dashBoardController/getAIPrompt.md deleted file mode 100644 index 2db7d13bf..000000000 --- a/requirements/dashBoardController/getAIPrompt.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if the GPT exists and send the results back -2. ✅ Returns 200 if there is no error and new GPT Prompt is created - -> ## Negative case - -1. ❌ Returns 500 if GPT Prompt does not exist -2. ❌ Returns 500 if there is an error in creating the GPT Prompt - -> ## Edge case \ No newline at end of file diff --git a/requirements/dashBoardController/getPromptCopiedDate.md b/requirements/dashBoardController/getPromptCopiedDate.md deleted file mode 100644 index 6b8bef08f..000000000 --- a/requirements/dashBoardController/getPromptCopiedDate.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if there is a user and return copied AI prompt - -> ## Negative case - - -> ## Edge case -1. Returns undefined when the user is not found \ No newline at end of file diff --git a/requirements/dashBoardController/getSuggestionOption.md b/requirements/dashBoardController/getSuggestionOption.md deleted file mode 100644 index cf2739853..000000000 --- a/requirements/dashBoardController/getSuggestionOption.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if there is suggestion data - -> ## Negative case - -1. ❌ Returns 404 if the suggestion data is not found -2. ❌ Returns 500 if there is an error in the function - -> ## Edge case diff --git a/requirements/dashBoardController/leaderboarddata.md b/requirements/dashBoardController/leaderboarddata.md deleted file mode 100644 index 6a7b36f0a..000000000 --- a/requirements/dashBoardController/leaderboarddata.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if there is leaderboard data -2. ✅ Returns 200 if leaderboard data is empty and returns getUserLaborData - -> ## Negative case - -1. ❌ Returns 400 if there is an error - -> ## Edge case diff --git a/requirements/dashBoardController/monthlydata.md b/requirements/dashBoardController/monthlydata.md deleted file mode 100644 index 7d464c242..000000000 --- a/requirements/dashBoardController/monthlydata.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if there is no results and return empty results -3. ✅ Returns 200 if there is results and return results - -> ## Negative case - -> ## Edge case \ No newline at end of file diff --git a/requirements/dashBoardController/orgData.md b/requirements/dashBoardController/orgData.md deleted file mode 100644 index 2d147a950..000000000 --- a/requirements/dashBoardController/orgData.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if the result is found and returns result - -> ## Negative case - -1. ❌ Returns 400 if there is an error in the function - -> ## Edge case diff --git a/requirements/dashBoardController/sendBugReport.md b/requirements/dashBoardController/sendBugReport.md deleted file mode 100644 index fd2f32c18..000000000 --- a/requirements/dashBoardController/sendBugReport.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if the bug report email is sent - -> ## Negative case - -1. ❌ Returns 500 if the email fails to send - -> ## Edge case diff --git a/requirements/dashBoardController/sendMakeSuggestion.md b/requirements/dashBoardController/sendMakeSuggestion.md deleted file mode 100644 index 1c425ac91..000000000 --- a/requirements/dashBoardController/sendMakeSuggestion.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ✅ Returns 200 if the suggestion email is sent successfully - -> ## Negative case - -1. ❌ Returns 500 if the suggestion email fails to send - -> ## Edge case diff --git a/requirements/dashBoardController/updateAIPrompt.md b/requirements/dashBoardController/updateAIPrompt.md deleted file mode 100644 index 1bb0202ec..000000000 --- a/requirements/dashBoardController/updateAIPrompt.md +++ /dev/null @@ -1,16 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if there is no error and AI Prompt is saved - -> ## Negative case - -1. ❌ Returns error 500 if the error occurs in the AI Prompt function - -> ## Edge case -1. Returns undefined if the requestor role is not an owner \ No newline at end of file diff --git a/requirements/dashBoardController/updateCopiedPrompt.md b/requirements/dashBoardController/updateCopiedPrompt.md deleted file mode 100644 index bc84a91c8..000000000 --- a/requirements/dashBoardController/updateCopiedPrompt.md +++ /dev/null @@ -1,16 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if there is no error and user is found - -> ## Negative case - -1. ❌ Returns error 404 if the user is not found -2. ❌ Returns error 500 if the error occurs in the file update function - -> ## Edge case diff --git a/requirements/dashBoardController/weeklydata.md b/requirements/dashBoardController/weeklydata.md deleted file mode 100644 index e774ee2dc..000000000 --- a/requirements/dashBoardController/weeklydata.md +++ /dev/null @@ -1,13 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if there is no error and labordata is found - -> ## Negative case - -> ## Edge case diff --git a/requirements/forcePwdController/forcePwd.md b/requirements/forcePwdController/forcePwd.md deleted file mode 100644 index ae1c5ae02..000000000 --- a/requirements/forcePwdController/forcePwd.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# ForcePwd - -> ## Negative Cases - -1. ✅ Returns a `400 Bad Request` status if userId is not valid with an error message "Bad Request". -2. ✅ Returns a `500 Internal Error` status with the error details if finding userProfile fails. -3. ✅ Returns a `500 Internal Error` status with the error details if new password fails to save. - -> ## Positive Cases - -1. ✅ Returns a `200 OK` status with a success message "password Reset". diff --git a/requirements/forgotPwdController/postForgotPwd.md b/requirements/forgotPwdController/postForgotPwd.md deleted file mode 100644 index 0d5349a26..000000000 --- a/requirements/forgotPwdController/postForgotPwd.md +++ /dev/null @@ -1,16 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Forgot Pwd - -> ## Positive case - -1. ✅ Receives a POST request in the **/api/forgotpassword** route. -2. ✅ Returns **200** if successfully temporary password generated. - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist. -2. ✅ Returns 400 user does not exists in database. -3. ✅ Returns 500 if error encountered fetching user details from database. -4. ✅ Returns 500 if error encountered while saving temporary password. \ No newline at end of file diff --git a/requirements/inventoryController/getAllInvInProject.md b/requirements/inventoryController/getAllInvInProject.md new file mode 100644 index 000000000..fdb38b0d2 --- /dev/null +++ b/requirements/inventoryController/getAllInvInProject.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ❌ Returns 200 if successfully fetch inventory data + + > ## Negative case + +3. ❌ Returns error 404 if the API does not exist +4. ❌ Returns error code 403 if the user is not authorized to view the inventory data +5. ❌ Returns error code 404 if an error occurs when populating or saving. + +> ## Edge case diff --git a/requirements/inventoryController/getAllInvInProjectWBS.md b/requirements/inventoryController/getAllInvInProjectWBS.md new file mode 100644 index 000000000..16eb0c22f --- /dev/null +++ b/requirements/inventoryController/getAllInvInProjectWBS.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if successfully found data + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns 403 if user is not authorized to view inventory data +3. ✅ Returns 404 if an error occurs while fetching data + +> ## Edge case diff --git a/requirements/inventoryController/postInvInProjectWBS.md b/requirements/inventoryController/postInvInProjectWBS.md new file mode 100644 index 000000000..bd863f9d7 --- /dev/null +++ b/requirements/inventoryController/postInvInProjectWBS.md @@ -0,0 +1,19 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns status code 201, if the inventory was successfully created and saved +3. ✅ Returns status code 201, if the inventory item was succesfully updated and saved. + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns error 403 if the user is not authorized to view data +3. ✅ Returns error 500 if an error occurs when saving +4. ✅ Returns error 400 if a valid project was found but quantity and type id were missing + +> ## Edge case diff --git a/requirements/logincontroller/getUser-usecase.md b/requirements/logincontroller/getUser-usecase.md deleted file mode 100644 index aa1dfd50c..000000000 --- a/requirements/logincontroller/getUser-usecase.md +++ /dev/null @@ -1,9 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# GetUser - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns **200**, with the requestor body diff --git a/requirements/logincontroller/login-usecase.md b/requirements/logincontroller/login-usecase.md deleted file mode 100644 index 6c0188caf..000000000 --- a/requirements/logincontroller/login-usecase.md +++ /dev/null @@ -1,21 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# login - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200, if the user is a new user and there is a password match -3. ✅ Returns 200, if the user already exists and the password is a match - -## Negative case - -1. ✅ Returns error 400 if there is no email or password -2. ✅ Returns error 403 if there is no user -3. ✅ Returns error 403 if the user exists but is not active -4. ✅ Returns error 403 if the password is not a match and if the user already exists - in progress - -## Edge case - -1. ✅ Returns the error if the try block fails - in progress \ No newline at end of file diff --git a/requirements/mapLocationsController/deleteLocation.md b/requirements/mapLocationsController/deleteLocation.md new file mode 100644 index 000000000..bbe2bf94b --- /dev/null +++ b/requirements/mapLocationsController/deleteLocation.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 403 if user is not authorized. +3. ✅ Returns 500 if an error occurs when deleting the map location. + +> ## Edge case diff --git a/requirements/mapLocationsController/getAllLocations.md b/requirements/mapLocationsController/getAllLocations.md new file mode 100644 index 000000000..e5edb3434 --- /dev/null +++ b/requirements/mapLocationsController/getAllLocations.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 404 if an error occurs when finding all users. +3. ✅ Returns 404 if an error occurs when finding all map locations. + +> ## Edge case diff --git a/requirements/mapLocationsController/putUserLocation.md b/requirements/mapLocationsController/putUserLocation.md new file mode 100644 index 000000000..a68706060 --- /dev/null +++ b/requirements/mapLocationsController/putUserLocation.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 403 if user is not authorized. +3. ✅ Returns 500 if an error occurs when saving the map location. + +> ## Edge case diff --git a/requirements/mapLocationsController/updateUserLocation.md b/requirements/mapLocationsController/updateUserLocation.md new file mode 100644 index 000000000..cf2de2f2e --- /dev/null +++ b/requirements/mapLocationsController/updateUserLocation.md @@ -0,0 +1,19 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful when `userType` is user and clears and resets cache. +3. ✅ Returns 200 if all is successful when `userType` is _not_ user. + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 403 if user is not authorized. +3. ✅ Returns 500 if an error occurs when updating the user location. +4. ✅ Returns 500 if an error occurs when updating the map location. + +> ## Edge case diff --git a/requirements/mouseoverTextController/createMouseoverText.md b/requirements/mouseoverTextController/createMouseoverText.md deleted file mode 100644 index 118803ae8..000000000 --- a/requirements/mouseoverTextController/createMouseoverText.md +++ /dev/null @@ -1,11 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# createMouseoverText - -> ## Positive case -1. ✅ Return 201 if create new mouseoverText successfully. - -> ## Negative case -1. ✅ Returns error 500 if any error when saving the new mouseoverText -> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/getMouseoverText.md b/requirements/mouseoverTextController/getMouseoverText.md deleted file mode 100644 index aee52b346..000000000 --- a/requirements/mouseoverTextController/getMouseoverText.md +++ /dev/null @@ -1,11 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# getMouseoverText - -> ## Positive case -1. ✅ Return 200 if find mouseoverText successfully. - -> ## Negative case -1. ✅ Returns error 404 if any error when finding the mouseoverText -> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/updateMouseoverText.md b/requirements/mouseoverTextController/updateMouseoverText.md deleted file mode 100644 index 84f786c26..000000000 --- a/requirements/mouseoverTextController/updateMouseoverText.md +++ /dev/null @@ -1,12 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# updateMouseoverText - -> ## Positive case -1. ✅ Return 201 if updating mouseoverText successfully. - -> ## Negative case -1. ✅ Returns error 500 if any error when finding the mouseoverText by Id -2. ✅ Returns error 400 if any error when saving the mouseoverText -> ## Edge case \ No newline at end of file diff --git a/requirements/notificationController/creatUserNotification.md b/requirements/notificationController/creatUserNotification.md deleted file mode 100644 index bbe81bee8..000000000 --- a/requirements/notificationController/creatUserNotification.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# Create User Notification - -## Negative Cases - -1. ✅ Returns error 403 if requestor role is not Admin or Owner -2. ✅ Returns error 400 if message and recipient are missing from request -3. ✅ Returns error 500 if there is an internal error while fetching unread notifications. - -## Positive Cases - -1. ✅ Returns status 200 when notification is successfully created with sender, recipient and message diff --git a/requirements/notificationController/deleteUserNotification.md b/requirements/notificationController/deleteUserNotification.md deleted file mode 100644 index f9cdabb80..000000000 --- a/requirements/notificationController/deleteUserNotification.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# Delete User Notification - -## Negative Cases - -1. ✅ Returns error 403 if requestor role is not Admin or Owner. -2. ✅ Returns error 500 if there is an internal error while deleting notification. - -## Positive Cases - -1. ✅ Returns status 200 when notification is successfully deleted. diff --git a/requirements/notificationController/getSentNotifications.md b/requirements/notificationController/getSentNotifications.md deleted file mode 100644 index c9d541fe7..000000000 --- a/requirements/notificationController/getSentNotifications.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# GET Sent Notifications - -## Positive Cases - -1. ✅ Returns status 200 Successful Data Retrieval - -## Negative Cases - -1. ✅ Returns error 403 if requestor role is not Admin or Owner -2. ✅ Returns error 500 if there is an internal error while fetching notifications. diff --git a/requirements/notificationController/getUnreadUserNotifications.md b/requirements/notificationController/getUnreadUserNotifications.md deleted file mode 100644 index a3321da76..000000000 --- a/requirements/notificationController/getUnreadUserNotifications.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# GET Unread User Notifications - -## Negative Cases - -1. ✅ Returns error 403 if userId does not match requestorId. -2. ✅ Returns error 400 if the userId is missing from the request. -3. ✅ Returns error 500 if there is an internal error while fetching unread notifications. - -## Positive Cases - -1. ✅ Returns status 200 with notification data when a valid userId is provided by an Administrator or Owner querying another user's notifications. diff --git a/requirements/notificationController/getUserNotifications.md b/requirements/notificationController/getUserNotifications.md deleted file mode 100644 index d16eba504..000000000 --- a/requirements/notificationController/getUserNotifications.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# GET User Notifications - -## Negative Cases - -1. ✅ Returns error 403 if userId does not match requestorId. -2. ✅ Returns error 400 if the userId is missing from the request. -3. ✅ Returns error 500 if there is an internal error while fetching notifications. - -## Positive Cases - -1. ✅ Returns status 200 with notification data when a valid userId is provided by an Administrator or Owner querying another user's notifications. diff --git a/requirements/notificationController/markNotificationAsRead.md b/requirements/notificationController/markNotificationAsRead.md deleted file mode 100644 index 3971e9086..000000000 --- a/requirements/notificationController/markNotificationAsRead.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - - -# Mark Notification as Read - -## Negative Cases - -1. ✅ Returns error 400 if recipientId is missing. -2. ✅ Returns error 500 if there is an internal error while reading notification. - -## Positive Cases - -1. ✅ Returns status 200 when notification is successfully read. diff --git a/requirements/ownerMessageController/deleteOwnerMessage.md b/requirements/ownerMessageController/deleteOwnerMessage.md deleted file mode 100644 index 1814f55f2..000000000 --- a/requirements/ownerMessageController/deleteOwnerMessage.md +++ /dev/null @@ -1,13 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# update Owner Messages - -> ## Negative Cases - -1. ✅ Returns error status 500 if an error occurs during the delete -2. ✅ Returns error status 403 if requestor is not an owner - -> ## Positive Cases - -1. ✅ Returns status 200 and deletes the owner message correctly diff --git a/requirements/ownerMessageController/getOwnerMessage.md b/requirements/ownerMessageController/getOwnerMessage.md deleted file mode 100644 index 0d2bd1bc1..000000000 --- a/requirements/ownerMessageController/getOwnerMessage.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get Owner Messages - -> ## Negative Cases - -1. ✅ Returns error status 404 if Owner Message cant be found - - -> ## Positive Cases - -1. ✅ Returns status 200 and initializes a new owner message if none exists. -2. ✅ Returns status 200 and returns the existing owner message if one or more exist. diff --git a/requirements/ownerMessageController/updateOwnerMessage.md b/requirements/ownerMessageController/updateOwnerMessage.md deleted file mode 100644 index 1f889e681..000000000 --- a/requirements/ownerMessageController/updateOwnerMessage.md +++ /dev/null @@ -1,13 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# update Owner Messages - -> ## Negative Cases - -1. ✅ Returns error status 500 if an error occurs during the update -2. ✅ Returns error status 403 if requestor is not an owner - -> ## Positive Cases - -1. ❌ Returns status 201 and updates the owner message correctly with new message diff --git a/requirements/rolePresetsController/createNewPresets.md b/requirements/rolePresetsController/createNewPresets.md deleted file mode 100644 index 7a6edc948..000000000 --- a/requirements/rolePresetsController/createNewPresets.md +++ /dev/null @@ -1,18 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# createNewPreset - -> ## Positive case -1. ✅ Receives a POST request in the **/api/rolePreset** route -2. ✅ Return 201 if create New Presets successfully. - -> ## Negative case - -1. ✅ Returns error 403 if user doesn't have permissions for putRole -2. ✅ Returns 400 if missing presetName -3. ✅ Returns 400 if missing roleName -4. ✅ Returns 400 if missing premissions -5. ✅ Returns error 400 when saving new presets - -> ## Edge case diff --git a/requirements/rolePresetsController/deletePresetById.md b/requirements/rolePresetsController/deletePresetById.md deleted file mode 100644 index 7698663fb..000000000 --- a/requirements/rolePresetsController/deletePresetById.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# deletePresetById - -> ## Positive case - -1. ✅ Return 200 if removing preset by id successfully. - -> ## Negative case - -1. ✅ Returns error 403 if user doesn't have permissions for putRole -2. ✅ Returns 400 if error in finding by id -3. ✅ Returns 400 if the route doesn't exist -4. ✅ Returns 400 if any error when removing results - -> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/getPresetsByRole.md b/requirements/rolePresetsController/getPresetsByRole.md deleted file mode 100644 index 7f14b828e..000000000 --- a/requirements/rolePresetsController/getPresetsByRole.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# getPresetsByRole - -> ## Positive case -1. ✅ Receives a GET request in the **/api/rolePreset** route -2. ✅ Return 200 if get Presets by roleName successfully. - -> ## Negative case - -1. ✅ Returns error 403 if user doesn't have permissions for putRole -2. ✅ Returns 400 when catching any error in finding roleName - -> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/updatePresetById.md b/requirements/rolePresetsController/updatePresetById.md deleted file mode 100644 index 6ce963cf3..000000000 --- a/requirements/rolePresetsController/updatePresetById.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# updatePresetById - -> ## Positive case - -1. ✅ Return 200 if update preset by id successfully. - -> ## Negative case - -1. ✅ Returns error 403 if user doesn't have permissions for putRole -2. ✅ Returns 400 if the router doesn't exist -3. ✅ Returns 400 if error in finding by id -3. ✅ Returns 400 if any error when saving results - -> ## Edge case \ No newline at end of file diff --git a/requirements/rolesController/createNewRole.md b/requirements/rolesController/createNewRole.md deleted file mode 100644 index 391bff025..000000000 --- a/requirements/rolesController/createNewRole.md +++ /dev/null @@ -1,24 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -## createNewRole Function - -> ### Positive case -1. ✅ Should return 201 and the new role on success - - Receives a POST request - - User has permission - - Mandatory fields are provided - - Successfully saves the new role to the database - -> ### Negative case -2. ✅ Should return 403 if user lacks permission - - Receives a POST request - - User does not have permission -3. ✅ Should return 400 if mandatory fields are missing - - Receives a POST request - - User has permission - - Mandatory fields are not provided -4. ✅ Should return 500 on role save error - - Receives a POST request - - User has permission - - Error occurs while saving the new role to the database diff --git a/requirements/rolesController/deleteRoleById.md b/requirements/rolesController/deleteRoleById.md deleted file mode 100644 index d7605d1c0..000000000 --- a/requirements/rolesController/deleteRoleById.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -## deleteRoleById Function - -> ### Positive case -1. ✅ Should return 200 and the deleted role on success - - Receives a DELETE request - - User has permission - - Successfully deletes the role from the database - -> ### Negative case -2. ✅ Should return 403 if user lacks permission - - Receives a DELETE request - - User does not have permission diff --git a/requirements/rolesController/getAllRoles.md b/requirements/rolesController/getAllRoles.md deleted file mode 100644 index a6acc35d5..000000000 --- a/requirements/rolesController/getAllRoles.md +++ /dev/null @@ -1,11 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -## getAllRoles Function - -> ### Positive case -1. ✅ Should return 200 and roles on success - - -> ### Negative case -1. ✅ Should return 404 on error when error occurs while retrieving roles from the database diff --git a/requirements/rolesController/getRolesById.md b/requirements/rolesController/getRolesById.md deleted file mode 100644 index 6ce3ac123..000000000 --- a/requirements/rolesController/getRolesById.md +++ /dev/null @@ -1,14 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -## getRoleById Function - -> ### Positive case -1. ✅ Should return 200 and the role on success - - Receives a GET request - - Successfully retrieves the role by ID from the database - -> ### Negative case -2. ✅ Should return 404 on error - - Receives a GET request - - Error occurs while retrieving the role by ID from the database diff --git a/requirements/rolesController/updateRoleById.md b/requirements/rolesController/updateRoleById.md deleted file mode 100644 index c38a04ecf..000000000 --- a/requirements/rolesController/updateRoleById.md +++ /dev/null @@ -1,28 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -## updateRoleById Function - -> ### Positive case -1. ✅ Should return 201 and the updated role on success - - Receives a PUT request - - User has permission - - Mandatory fields are provided - - Successfully updates the role in the database - -> ### Negative case -2. ✅ Should return 403 if user lacks permission - - Receives a PUT request - - User does not have permission -3. ✅ Should return 400 if mandatory fields are missing - - Receives a PUT request - - User has permission - - Mandatory fields are not provided -4. ✅ Should return 400 if no valid records are found - - Receives a PUT request - - User has permission - - No valid records are found to update -5. ✅ Should return 500 on role save error - - Receives a PUT request - - User has permission - - Error occurs while saving the updated role to the database diff --git a/requirements/timeOffRequestController/deleteTimeOffRequestById.md b/requirements/timeOffRequestController/deleteTimeOffRequestById.md deleted file mode 100644 index 5e832c81d..000000000 --- a/requirements/timeOffRequestController/deleteTimeOffRequestById.md +++ /dev/null @@ -1,21 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Delete Time Off Request By Id - -> ## Positive case - -1. ✅ Returns 200 on successfully deleting the request. - -> ## Negative case - -1. ✅ Returns 403 if the delete request is made my a user for whom all of the below cases are true: - a. User does not have the role of Owner nor of Administrator. - b. User is attempting to delete someone else's timeOffRequest. - c. User does not have the 'manageTimeOffRequests' permission. - -2. ✅ Returns 404 if the timeOffRequest is not found. - -3. ✅ Returns 500 if the some any occured while deleting or checking for permission or any other case. - -> ## Edge case diff --git a/requirements/timeOffRequestController/getTimeOffRequestById.md b/requirements/timeOffRequestController/getTimeOffRequestById.md deleted file mode 100644 index 633fed63f..000000000 --- a/requirements/timeOffRequestController/getTimeOffRequestById.md +++ /dev/null @@ -1,15 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get Time Off Request By Id - -> ## Positive case - -1. ✅ Returns all time-off request and status code 200 if successful. - -> ## Negative case - -1. ✅ Return status code 500, if any error is encountered while fetching time-off request using an Id. -2. ✅ Return status code 404, if no time-off request exists with the requested Id. - -> ## Edge case diff --git a/requirements/timeOffRequestController/getTimeOffRequests.md b/requirements/timeOffRequestController/getTimeOffRequests.md deleted file mode 100644 index dd54d5e69..000000000 --- a/requirements/timeOffRequestController/getTimeOffRequests.md +++ /dev/null @@ -1,16 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get Time Off Requests - -> ## Positive case - -1. ✅ Returns formatted all time-off requests and status code 200 if successful. -2. - -> ## Negative case - -1. ✅ Return status code 500, if any error is encountered while fetching all time-off requests. -2. - -> ## Edge case diff --git a/requirements/timeOffRequestController/setTimeOffRequest.md b/requirements/timeOffRequestController/setTimeOffRequest.md deleted file mode 100644 index 4f6e6c81e..000000000 --- a/requirements/timeOffRequestController/setTimeOffRequest.md +++ /dev/null @@ -1,20 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get Time Off Requests - -> ## Positive case - -1. ✅ Returns status code 201, if the new time-off request is saved successfully. - -> ## Negative case - -1. ✅ Return status code 403, if the user is not authorized to set time-off request. -2. ✅ Return status code 400, if the request is missing any of the following parameters: - a. duration - b. startingDate - c. reason - d. requestFor -3. ✅ Return status code 500, if any error occurs while setting the time-off request. - -> ## Edge case diff --git a/requirements/timeOffRequestController/updateTimeOffRequestById.md b/requirements/timeOffRequestController/updateTimeOffRequestById.md deleted file mode 100644 index 3e7ef74df..000000000 --- a/requirements/timeOffRequestController/updateTimeOffRequestById.md +++ /dev/null @@ -1,26 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Delete Time Off Request By Id - -> ## Positive case - -1. ✅ Returns 200 if the timeOffRequest is successfully updated - -> ## Negative case - -1. ✅ Returns 403 if the delete request is made my a user for whom all of the below cases are true: - a. User does not have the role of Owner nor of Administrator. - b. User does not have the 'manageTimeOffRequests' permission. - -2. ✅ Returns 400 is request body is contains one of the following parameters incorrect: - a. duration - b. reason - c. startingDate - d. requestId - -3. ✅ Returns 404 if no timeOffRequest is found matching the requestId - -4. ✅ Returns 500 if any error occurs - -> ## Edge case diff --git a/src/app.js b/src/app.js index 359db89c2..a6d0abd00 100644 --- a/src/app.js +++ b/src/app.js @@ -3,22 +3,13 @@ const Sentry = require('@sentry/node'); const app = express(); const logger = require('./startup/logger'); -const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler').default; logger.init(); - // The request handler must be the first middleware on the app app.use(Sentry.Handlers.requestHandler()); - require('./startup/cors')(app); require('./startup/bodyParser')(app); require('./startup/middleware')(app); require('./startup/routes')(app); -// The error handler must be before any other error middleware and after all controllers -app.use(Sentry.Handlers.errorHandler()); - -// Make it the last middleware since it returns a response and do not call next() -app.use(globalErrorHandler); - -module.exports = { app, logger }; +module.exports = { app, logger, Sentry }; diff --git a/src/controllers/BlueSquareEmailAssignmentController.js b/src/controllers/BlueSquareEmailAssignmentController.js deleted file mode 100644 index 429e4b8ef..000000000 --- a/src/controllers/BlueSquareEmailAssignmentController.js +++ /dev/null @@ -1,67 +0,0 @@ -const BlueSquareEmailAssignmentController = function (BlueSquareEmailAssignment, userProfile) { - const getBlueSquareEmailAssignment = async function (req, res) { - try { - const assignments = await BlueSquareEmailAssignment.find().populate('assignedTo').exec() - res.status(200).send(assignments); - } catch (error) { - console.log(error) - res.status(500).send(error); - } - }; - - const setBlueSquareEmailAssignment = async function (req, res) { - try { - const { email } = req.body; - - if (!email) { - res.status(400).send('bad request'); - return; - } - - const user = await userProfile.findOne({ email }); - if (!userProfile) { - return res.status(400).send('User profile not found'); - } - - const newAssignment = new BlueSquareEmailAssignment({ - email, - assignedTo: user._id, - }); - await newAssignment.save(); - const assignment = await BlueSquareEmailAssignment.find({email}).populate('assignedTo').exec() - - res.status(200).send(assignment[0]); - } catch (error) { - res.status(500).send(error); - } - }; - - const deleteBlueSquareEmailAssignment = async function (req, res) { - try { - const { id } = req.params; - - if (!id) { - res.status(400).send('bad request'); - return; - } - - const deletedAssignment = await BlueSquareEmailAssignment.findOneAndDelete({ _id: id }); - if (!deletedAssignment) { - res.status(404).send('Assignment not found'); - return; - } - - res.status(200).send({id}); - } catch (error) { - res.status(500).send(error); - } - }; - - return { - getBlueSquareEmailAssignment, - setBlueSquareEmailAssignment, - deleteBlueSquareEmailAssignment, - }; -}; - -module.exports = BlueSquareEmailAssignmentController; diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 55c661b2c..14c72c76f 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -3,7 +3,6 @@ const UserProfile = require('../models/userProfile'); const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); const cacheClosure = require('../utilities/nodeCache'); -// const userHelper = require('../helpers/userHelper')(); const badgeController = function (Badge) { /** @@ -13,27 +12,11 @@ const badgeController = function (Badge) { */ const cache = cacheClosure(); - // const awardBadgesTest = async function (req, res) { - // await userHelper.awardNewBadges(); - // res.status(200).send('Badges awarded'); - // }; - const getAllBadges = async function (req, res) { - console.log(req.body.requestor); // Retain logging from development branch for debugging - - // Check if the user has any of the following permissions - if ( - !(await helper.hasPermission(req.body.requestor, 'seeBadges')) && - !(await helper.hasPermission(req.body.requestor, 'assignBadges')) && - !(await helper.hasPermission(req.body.requestor, 'createBadges')) && - !(await helper.hasPermission(req.body.requestor, 'updateBadges')) && - !(await helper.hasPermission(req.body.requestor, 'deleteBadges')) - ) { - console.log('in if statement'); // Retain logging from development branch for debugging + if (!(await helper.hasPermission(req.body.requestor, 'seeBadges')) && !(await helper.hasPermission(req.body.requestor, 'assignBadges'))) { res.status(403).send('You are not authorized to view all badge data.'); return; } - // Add cache to reduce database query and optimize performance if (cache.hasCache('allBadges')) { res.status(200).send(cache.getCache('allBadges')); @@ -56,7 +39,7 @@ const badgeController = function (Badge) { cache.setCache('allBadges', results); res.status(200).send(results); }) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); }; /** @@ -87,13 +70,6 @@ const badgeController = function (Badge) { res.status(400).send('Can not find the user to be assigned.'); return; } - let totalNewBadges = 0; - const existingBadges = {}; - if (record.badgeCollection && Array.isArray(record.badgeCollection)) { - record.badgeCollection.forEach(badgeItem => { - existingBadges[badgeItem.badge] = badgeItem.count; - }); - } const badgeGroups = req.body.badgeCollection.reduce((grouped, item) => { const { badge } = item; @@ -109,7 +85,6 @@ const badgeController = function (Badge) { return grouped; } - if (!grouped[badge]) { // If the badge is not in the grouped object, add a new entry grouped[badge] = { @@ -137,11 +112,6 @@ const badgeController = function (Badge) { ); } } - if (existingBadges[badge]) { - totalNewBadges += Math.max(0, item.count - existingBadges[badge]); - } else { - totalNewBadges += item.count; - } return grouped; }, {}); @@ -156,7 +126,6 @@ const badgeController = function (Badge) { })); record.badgeCollection = badgeGroupsArray; - record.badgeCount += totalNewBadges; if (cache.hasCache(`user-${userToBeAssigned}`)) { cache.removeCache(`user-${userToBeAssigned}`); @@ -293,68 +262,14 @@ const badgeController = function (Badge) { res.status(200).send({ message: 'Badge successfully updated' }); }); }; - const getBadgeCount = async function (req, res) { - const userId = mongoose.Types.ObjectId(req.params.userId); - - UserProfile.findById(userId, (error, record) => { - // Check for errors or if user profile doesn't exist - if (error || record === null) { - res.sendStatus(404).send('Can not find the user to be assigned.'); - return; - } - // Return badge count from user profile - res.status(200).send({ count: record.badgeCount }); - }); - } - - - const putBadgecount = async function (req, res) { - const userId = mongoose.Types.ObjectId(req.params.userId); - - UserProfile.findById(userId, (error, record) => { - if (error || record === null) { - res.status(400).send('Can not find the user to be assigned.'); - return; - } - record.badgeCount = 1; - - record - .save() - .then(results => res.status(201).send(results._id)) - .catch((err) => { - res.status(500).send(err); - }); - }); - }; - - const resetBadgecount = async function (req, res) { - const userId = mongoose.Types.ObjectId(req.params.userId); - - UserProfile.findById(userId, (error, record) => { - if (error || record === null) { - res.status(400).send('Can not find the user to be assigned.'); - return; - } - record.badgeCount = 0; - - record.save(); - res.status(201).send({ count: record.badgeCount }); - - }); - } - return { - // awardBadgesTest, getAllBadges, assignBadges, postBadge, deleteBadge, putBadge, - getBadgeCount, - putBadgecount, - resetBadgecount }; }; -module.exports = badgeController; \ No newline at end of file +module.exports = badgeController; diff --git a/src/controllers/badgeController.spec.js b/src/controllers/badgeController.spec.js index 3c0c92b7f..0149bee49 100644 --- a/src/controllers/badgeController.spec.js +++ b/src/controllers/badgeController.spec.js @@ -1,7 +1,6 @@ +// const mongoose = require('mongoose'); +// const UserProfile = require('../models/userProfile'); const mongoose = require('mongoose'); -// mock the cache function before importing so we can manipulate the implementation -jest.mock('../utilities/nodeCache'); -const cache = require('../utilities/nodeCache'); const Badge = require('../models/badge'); const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); @@ -9,17 +8,23 @@ const badgeController = require('./badgeController'); const { mockReq, mockRes, assertResMock } = require('../test'); const UserProfile = require('../models/userProfile'); +// mock the cache function before importing so we can manipulate the implementation +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); + const makeSut = () => { - const { postBadge, getAllBadges, assignBadges, deleteBadge } = badgeController(Badge); + const { postBadge, getAllBadges, assignBadges, deleteBadge, putBadge } = badgeController(Badge); - return { postBadge, getAllBadges, assignBadges, deleteBadge }; + return { postBadge, getAllBadges, assignBadges, deleteBadge, putBadge }; }; +// Allows us to test functions using promise chaining. const flushPromises = () => new Promise(setImmediate); const mockHasPermission = (value) => jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); +// eslint-disable-next-line no-unused-vars const makeMockCache = (method, value) => { const cacheObject = { getCache: jest.fn(), @@ -165,81 +170,83 @@ describe('badeController module', () => { assertResMock(500, new Error(errorMsg), response, mockRes); }); - test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { - const { mockCache: getCacheMock } = makeMockCache('getCache', ''); - const { postBadge } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - - const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - const newBadge = { - badgeName: mockReq.body.badgeName, - category: mockReq.body.category, - multiple: mockReq.body.multiple, - totalHrs: mockReq.body.totalHrs, - weeks: mockReq.body.weeks, - months: mockReq.body.months, - people: mockReq.body.people, - project: mockReq.body.project, - imageUrl: mockReq.body.imageUrl, - ranking: mockReq.body.ranking, - description: mockReq.body.description, - showReport: mockReq.body.showReport, - }; - - jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - const response = await postBadge(mockReq, mockRes); - - expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - expect(findSpy).toHaveBeenCalledWith({ - badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - }); - assertResMock(201, newBadge, response, mockRes); - }); - - test('Clears cache if all is successful and there is a badge cache', async () => { - const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); - const removeCacheMock = jest - .spyOn(cacheObject, 'removeCache') - .mockImplementationOnce(() => null); - const { postBadge } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - - const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - const newBadge = { - badgeName: mockReq.body.badgeName, - category: mockReq.body.category, - multiple: mockReq.body.multiple, - totalHrs: mockReq.body.totalHrs, - weeks: mockReq.body.weeks, - months: mockReq.body.months, - people: mockReq.body.people, - project: mockReq.body.project, - imageUrl: mockReq.body.imageUrl, - ranking: mockReq.body.ranking, - description: mockReq.body.description, - showReport: mockReq.body.showReport, - }; - - jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - const response = await postBadge(mockReq, mockRes); - - expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - expect(findSpy).toHaveBeenCalledWith({ - badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - }); - assertResMock(201, newBadge, response, mockRes); - }); + // test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { + // const { mockCache: getCacheMock } = makeMockCache('getCache', ''); + // const { postBadge } = makeSut(); + // const hasPermissionSpy = mockHasPermission(true); + + // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + // const newBadge = { + // badgeName: mockReq.body.badgeName, + // category: mockReq.body.category, + // multiple: mockReq.body.multiple, + // totalHrs: mockReq.body.totalHrs, + // weeks: mockReq.body.weeks, + // months: mockReq.body.months, + // people: mockReq.body.people, + // project: mockReq.body.project, + // imageUrl: mockReq.body.imageUrl, + // ranking: mockReq.body.ranking, + // description: mockReq.body.description, + // showReport: mockReq.body.showReport, + // }; + + // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + // const response = await postBadge(mockReq, mockRes); + + // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + // expect(findSpy).toHaveBeenCalledWith({ + // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + // }); + // assertResMock(201, newBadge, response, mockRes); + // }); + + // test('Clears cache if all is successful and there is a badge cache', async () => { + // const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); + // const removeCacheMock = jest + // .spyOn(cacheObject, 'removeCache') + // .mockImplementationOnce(() => null); + // const { postBadge } = makeSut(); + // const hasPermissionSpy = mockHasPermission(true); + + // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + // const newBadge = { + // badgeName: mockReq.body.badgeName, + // category: mockReq.body.category, + // multiple: mockReq.body.multiple, + // totalHrs: mockReq.body.totalHrs, + // weeks: mockReq.body.weeks, + // months: mockReq.body.months, + // people: mockReq.body.people, + // project: mockReq.body.project, + // imageUrl: mockReq.body.imageUrl, + // ranking: mockReq.body.ranking, + // description: mockReq.body.description, + // showReport: mockReq.body.showReport, + // }; + + // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + // const response = await postBadge(mockReq, mockRes); + + // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + // expect(findSpy).toHaveBeenCalledWith({ + // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + // }); + // assertResMock(201, newBadge, response, mockRes); + // }); }); describe('getAllBadges method', () => { + // eslint-disable-next-line no-unused-vars const findObject = { populate: () => {} }; + // eslint-disable-next-line no-unused-vars const populateObject = { sort: () => {} }; test('Returns 403 if the user is not authorized', async () => { const { getAllBadges } = makeSut(); @@ -253,93 +260,93 @@ describe('badeController module', () => { expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); }); - test('Returns 500 if an error occurs when querying DB', async () => { - const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - const { getAllBadges } = makeSut(); - const mockPermission = mockHasPermission(true); - const errorMsg = 'Error when finding badges'; - - const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - const populateMock = jest - .spyOn(findObject, 'populate') - .mockImplementationOnce(() => populateObject); - const sortMock = jest - .spyOn(populateObject, 'sort') - .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); - - getAllBadges(mockReq, mockRes); - await flushPromises(); - - expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); - expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - expect(findMock).toHaveBeenCalledWith( - {}, - 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - ); - expect(populateMock).toHaveBeenCalledWith({ - path: 'project', - select: '_id projectName', - }); - expect(sortMock).toHaveBeenCalledWith({ - ranking: 1, - badgeName: 1, - }); - }); - - test('Returns 200 if the badges are in cache', async () => { - const badges = [{ badge: 'random badge' }]; - const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); - - const { getAllBadges } = makeSut(); - - const mockPermission = mockHasPermission(true); - - const response = await getAllBadges(mockReq, mockRes); - await flushPromises(); - - assertResMock(200, badges, response, mockRes); - expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - }); - - test('Returns 200 if not in cache, and all the async code succeeds.', async () => { - const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - const { getAllBadges } = makeSut(); - const mockPermission = mockHasPermission(true); - const badges = [{ badge: 'random badge' }]; - - const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - const populateMock = jest - .spyOn(findObject, 'populate') - .mockImplementationOnce(() => populateObject); - const sortMock = jest - .spyOn(populateObject, 'sort') - .mockImplementationOnce(() => Promise.resolve(badges)); - - getAllBadges(mockReq, mockRes); - await flushPromises(); - - expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith(badges); - expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - expect(findMock).toHaveBeenCalledWith( - {}, - 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - ); - expect(populateMock).toHaveBeenCalledWith({ - path: 'project', - select: '_id projectName', - }); - expect(sortMock).toHaveBeenCalledWith({ - ranking: 1, - badgeName: 1, - }); - }); + // test('Returns 500 if an error occurs when querying DB', async () => { + // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + // const { getAllBadges } = makeSut(); + // const mockPermission = mockHasPermission(true); + // const errorMsg = 'Error when finding badges'; + + // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + // const populateMock = jest + // .spyOn(findObject, 'populate') + // .mockImplementationOnce(() => populateObject); + // const sortMock = jest + // .spyOn(populateObject, 'sort') + // .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + + // getAllBadges(mockReq, mockRes); + // await flushPromises(); + + // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(mockRes.status).toHaveBeenCalledWith(500); + // expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); + // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + // expect(findMock).toHaveBeenCalledWith( + // {}, + // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + // ); + // expect(populateMock).toHaveBeenCalledWith({ + // path: 'project', + // select: '_id projectName', + // }); + // expect(sortMock).toHaveBeenCalledWith({ + // ranking: 1, + // badgeName: 1, + // }); + // }); + + // test('Returns 200 if the badges are in cache', async () => { + // const badges = [{ badge: 'random badge' }]; + // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + // const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); + + // const { getAllBadges } = makeSut(); + + // const mockPermission = mockHasPermission(true); + + // const response = await getAllBadges(mockReq, mockRes); + // await flushPromises(); + + // assertResMock(200, badges, response, mockRes); + // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + // }); + + // test('Returns 200 if not in cache, and all the async code succeeds.', async () => { + // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + // const { getAllBadges } = makeSut(); + // const mockPermission = mockHasPermission(true); + // const badges = [{ badge: 'random badge' }]; + + // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + // const populateMock = jest + // .spyOn(findObject, 'populate') + // .mockImplementationOnce(() => populateObject); + // const sortMock = jest + // .spyOn(populateObject, 'sort') + // .mockImplementationOnce(() => Promise.resolve(badges)); + + // getAllBadges(mockReq, mockRes); + // await flushPromises(); + + // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + // expect(mockRes.status).toHaveBeenCalledWith(200); + // expect(mockRes.send).toHaveBeenCalledWith(badges); + // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + // expect(findMock).toHaveBeenCalledWith( + // {}, + // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + // ); + // expect(populateMock).toHaveBeenCalledWith({ + // path: 'project', + // select: '_id projectName', + // }); + // expect(sortMock).toHaveBeenCalledWith({ + // ranking: 1, + // badgeName: 1, + // }); + // }); }); describe('assignBadges method', () => { @@ -385,72 +392,72 @@ describe('badeController module', () => { expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); }); - test('Returns 500 if an error occurs when saving edited user profile', async () => { - const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + // test('Returns 500 if an error occurs when saving edited user profile', async () => { + // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - const { assignBadges } = makeSut(); + // const { assignBadges } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - const errMsg = 'Error when saving'; - const findObj = { save: () => {} }; - const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); + // const hasPermissionSpy = mockHasPermission(true); + // const errMsg = 'Error when saving'; + // const findObj = { save: () => { } }; + // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + // jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); - const response = await assignBadges(mockReq, mockRes); + // const response = await assignBadges(mockReq, mockRes); - assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); - expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - expect(hasCacheMock).toHaveBeenCalledWith( - `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - ); + // assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); + // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + // expect(hasCacheMock).toHaveBeenCalledWith( + // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + // ); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - }); + // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + // }); - test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { - const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); + // test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { + // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + // const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); - const { assignBadges } = makeSut(); + // const { assignBadges } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - const findObj = { save: () => {} }; - const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + // const hasPermissionSpy = mockHasPermission(true); + // const findObj = { save: () => { } }; + // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); - const response = await assignBadges(mockReq, mockRes); + // const response = await assignBadges(mockReq, mockRes); - assertResMock(201, `randomId`, response, mockRes); - expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - expect(hasCacheMock).toHaveBeenCalledWith( - `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - ); - expect(removeCacheMock).toHaveBeenCalledWith( - `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - ); + // assertResMock(201, `randomId`, response, mockRes); + // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + // expect(hasCacheMock).toHaveBeenCalledWith( + // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + // ); + // expect(removeCacheMock).toHaveBeenCalledWith( + // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + // ); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - }); + // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + // }); - test('Returns 201 and if successful and user does not exist in cache', async () => { - const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + // test('Returns 201 and if successful and user does not exist in cache', async () => { + // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - const { assignBadges } = makeSut(); + // const { assignBadges } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - const findObj = { save: () => {} }; - const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + // const hasPermissionSpy = mockHasPermission(true); + // const findObj = { save: () => { } }; + // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); - const response = await assignBadges(mockReq, mockRes); + // const response = await assignBadges(mockReq, mockRes); - assertResMock(201, `randomId`, response, mockRes); - expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - expect(hasCacheMock).toHaveBeenCalledWith( - `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - ); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - }); + // assertResMock(201, `randomId`, response, mockRes); + // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + // expect(hasCacheMock).toHaveBeenCalledWith( + // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + // ); + // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + // }); }); describe('deleteBadge method', () => { @@ -623,4 +630,89 @@ describe('badeController module', () => { expect(removeCacheSpy).toHaveBeenCalledWith('allBadges'); }); }); + + describe('putBadge method', () => { + test('Returns 403 if the user is not authorized', async () => { + const { putBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + + const response = await putBadge(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, { error: 'You are not authorized to update badges.' }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); + }); + + test('Returns 400 if an error occurs in findById', async () => { + const { putBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findByIdAndUpdateSpy = jest + .spyOn(Badge, 'findByIdAndUpdate') + .mockImplementationOnce((_, __, cb) => cb(true, true)); + + const response = await putBadge(mockReq, mockRes); + await flushPromises(); + + const data = { + badgeName: mockReq.body.name || mockReq.body.badgeName, + description: mockReq.body.description, + type: mockReq.body.type, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + people: mockReq.body.people, + category: mockReq.body.category, + months: mockReq.body.months, + weeks: mockReq.body.weeks, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl || mockReq.body.imageURL, + ranking: mockReq.body.ranking, + showReport: mockReq.body.showReport, + }; + + expect(findByIdAndUpdateSpy).toHaveBeenCalledWith( + mockReq.params.badgeId, + data, + expect.anything(), + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); + assertResMock(400, { error: 'No valid records found' }, response, mockRes); + }); + + test('Returns 400 if no badge is found', async () => { + const { putBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findByIdAndUpdateSpy = jest + .spyOn(Badge, 'findByIdAndUpdate') + .mockImplementationOnce((_, __, cb) => cb(false, null)); + + const response = await putBadge(mockReq, mockRes); + await flushPromises(); + + const data = { + badgeName: mockReq.body.name || mockReq.body.badgeName, + description: mockReq.body.description, + type: mockReq.body.type, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + people: mockReq.body.people, + category: mockReq.body.category, + months: mockReq.body.months, + weeks: mockReq.body.weeks, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl || mockReq.body.imageURL, + ranking: mockReq.body.ranking, + showReport: mockReq.body.showReport, + }; + + expect(findByIdAndUpdateSpy).toHaveBeenCalledWith( + mockReq.params.badgeId, + data, + expect.anything(), + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); + assertResMock(400, { error: 'No valid records found' }, response, mockRes); + }); + }); }); diff --git a/src/controllers/bmdashboard/bmEquipmentController.js b/src/controllers/bmdashboard/bmEquipmentController.js index d3230082e..1255493ca 100644 --- a/src/controllers/bmdashboard/bmEquipmentController.js +++ b/src/controllers/bmdashboard/bmEquipmentController.js @@ -54,45 +54,6 @@ const bmEquipmentController = (BuildingEquipment) => { } }; - const fetchBMEquipments = async (req, res) => { - try { - BuildingEquipment - .find() - .populate([ - { - path: 'project', - select: '_id name', - }, - { - path: 'itemType', - select: '_id name', - }, - { - path: 'updateRecord', - populate: { - path: 'createdBy', - select: '_id firstName lastName', - }, - }, - { - path: 'purchaseRecord', - populate: { - path: 'requestedBy', - select: '_id firstName lastName', - }, - }, - ]) - .exec() - .then((result) => { - res.status(200).send(result); - }) - .catch((error) => res.status(500).send(error)); - } catch (err) { - res.json(err); - } -}; - - const bmPurchaseEquipments = async function (req, res) { const { projectId, @@ -142,7 +103,6 @@ const bmEquipmentController = (BuildingEquipment) => { return { fetchSingleEquipment, bmPurchaseEquipments, - fetchBMEquipments, }; }; diff --git a/src/controllers/bmdashboard/bmInventoryTypeController.js b/src/controllers/bmdashboard/bmInventoryTypeController.js index 175d948b4..f4cd6cf99 100644 --- a/src/controllers/bmdashboard/bmInventoryTypeController.js +++ b/src/controllers/bmdashboard/bmInventoryTypeController.js @@ -32,39 +32,12 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } const fetchToolTypes = async (req, res) => { - try { - ToolType - .find() - .populate([ - { - path: 'available', - select: '_id code project', - populate: { - path: 'project', - select: '_id name' - } - }, - { - path: 'using', - select: '_id code project', - populate: { - path: 'project', - select: '_id name' - } - } - ]) + ToolType.find() .exec() - .then(result => { - res.status(200).send(result); - }) - .catch(error => { - console.error("fetchToolTypes error: ", error); - res.status(500).send(error); - }); - + .then((result) => res.status(200).send(result)) + .catch((error) => res.status(500).send(error)); } catch (err) { - console.log("error: ", err) res.json(err); } }; @@ -201,75 +174,6 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } } - async function addToolType(req, res) { - const { - name, - description, - invoice, - purchaseRental, - fromDate, - toDate, - condition, - phoneNumber, - quantity, - currency, - unitPrice, - shippingFee, - taxes, - totalPriceWithShipping, - images, - link, - requestor: { requestorId }, - } = req.body; - - try { - ToolType.find({ name }) - .then((result) => { - if (result.length) { - res.status(409).send('Oops!! Tool already exists!'); - } else { - const newDoc = { - category: 'Tool', - name, - description, - invoice, - purchaseRental, - fromDate, - toDate, - condition, - phoneNumber, - quantity, - currency, - unitPrice, - shippingFee, - taxes, - totalPriceWithShipping, - images, - link, - createdBy: requestorId, - }; - ToolType.create(newDoc) - .then((results) => { - res.status(201).send(results); - }) - .catch((error) => { - if (error._message.includes('validation failed')) { - res.status(400).send(error.errors.unit.message); - } else { - res.status(500).send(error); - } - }); - } - }) - .catch((error) => { - res.status(500).send(error); - }); - } catch (error) { - res.status(500).send(error); - } - } - - async function fetchInventoryByType(req, res) { const { type } = req.params; let SelectedType = InvType; @@ -341,18 +245,6 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp res.status(500).send(error); } } - - async function fetchEquipmentTypes(req, res) { - try { - EquipType.find() - .exec() - .then((result) => res.status(200).send(result)) - .catch((error) => res.status(500).send(error)); - } catch (err) { - res.json(err); - } - } - const fetchSingleInventoryType = async (req, res) => { const { invtypeId } = req.params; try { @@ -398,12 +290,10 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp fetchReusableTypes, fetchToolTypes, addEquipmentType, - fetchEquipmentTypes, fetchSingleInventoryType, updateNameAndUnit, addMaterialType, addConsumableType, - addToolType, fetchInvUnitsFromJson, fetchInventoryByType, }; diff --git a/src/controllers/bmdashboard/bmReusableController.js b/src/controllers/bmdashboard/bmReusableController.js index 83cd19475..e4ae0574f 100644 --- a/src/controllers/bmdashboard/bmReusableController.js +++ b/src/controllers/bmdashboard/bmReusableController.js @@ -3,10 +3,10 @@ const { reusableType: ReusableType, } = require('../../models/bmdashboard/buildingInventoryType'); -// function isValidDate(dateString) { -// const date = new Date(dateString); -// return !isNaN(date.getTime()); -// } +function isValidDate(dateString) { + const date = new Date(dateString); + return !isNaN(date.getTime()); +} const bmReusableController = function (BuildingReusable) { const fetchBMReusables = async (req, res) => { @@ -103,154 +103,9 @@ const bmReusableController = function (BuildingReusable) { } }; - const bmPostReusableUpdateRecord = function (req, res) { - const payload = req.body; - let quantityUsed = +req.body.quantityUsed; - let quantityWasted = +req.body.quantityWasted; - const { reusable } = req.body; - if (payload.QtyUsedLogUnit === "percent" && quantityWasted >= 0) { - quantityUsed = +((+quantityUsed / 100) * reusable.stockAvailable).toFixed( - 4 - ); - } - if (payload.QtyWastedLogUnit === "percent" && quantityUsed >= 0) { - quantityWasted = +( - (+quantityWasted / 100) * - reusable.stockAvailable - ).toFixed(4); - } - - if ( - quantityUsed > reusable.stockAvailable || - quantityWasted > reusable.stockAvailable || - quantityUsed + quantityWasted > reusable.stockAvailable - ) { - res - .status(500) - .send( - "Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available." - ); - } else { - let newStockUsed = +reusable.stockUsed + parseFloat(quantityUsed); - let newStockWasted = +reusable.stockWasted + parseFloat(quantityWasted); - let newAvailable = - +reusable.stockAvailable - - parseFloat(quantityUsed) - - parseFloat(quantityWasted); - newStockUsed = parseFloat(newStockUsed.toFixed(4)); - newStockWasted = parseFloat(newStockWasted.toFixed(4)); - newAvailable = parseFloat(newAvailable.toFixed(4)); - BuildingReusable.updateOne( - { _id: req.body.reusable._id }, - - { - $set: { - stockUsed: newStockUsed, - stockWasted: newStockWasted, - stockAvailable: newAvailable, - }, - $push: { - updateRecord: { - date: req.body.date, - createdBy: req.body.requestor.requestorId, - quantityUsed, - quantityWasted, - }, - }, - } - ) - .then((results) => { - res.status(200).send(results); - }) - .catch((error) => res.status(500).send({ message: error })); - } - }; - - const bmPostReusableUpdateBulk = function (req, res) { - const reusableUpdates = req.body.upadateReusables; - let errorFlag = false; - const updateRecordsToBeAdded = []; - for (let i = 0; i < reusableUpdates.length; i+=1) { - const payload = reusableUpdates[i]; - let quantityUsed = +payload.quantityUsed; - let quantityWasted = +payload.quantityWasted; - const { reusable } = payload; - if (payload.QtyUsedLogUnit === "percent" && quantityWasted >= 0) { - quantityUsed = +( - (+quantityUsed / 100) * - reusable.stockAvailable - ).toFixed(4); - } - if (payload.QtyWastedLogUnit === "percent" && quantityUsed >= 0) { - quantityWasted = +( - (+quantityWasted / 100) * - reusable.stockAvailable - ).toFixed(4); - } - - let newStockUsed = +reusable.stockUsed + parseFloat(quantityUsed); - let newStockWasted = +reusable.stockWasted + parseFloat(quantityWasted); - let newAvailable = - +reusable.stockAvailable - - parseFloat(quantityUsed) - - parseFloat(quantityWasted); - newStockUsed = parseFloat(newStockUsed.toFixed(4)); - newStockWasted = parseFloat(newStockWasted.toFixed(4)); - newAvailable = parseFloat(newAvailable.toFixed(4)); - if (newAvailable < 0) { - errorFlag = true; - break; - } - updateRecordsToBeAdded.push({ - updateId: reusable._id, - set: { - stockUsed: newStockUsed, - stockWasted: newStockWasted, - stockAvailable: newAvailable, - }, - updateValue: { - createdBy: req.body.requestor.requestorId, - quantityUsed, - quantityWasted, - date: req.body.date, - }, - }); - } - - try { - if (errorFlag) { - res.status(500).send("Stock quantities submitted seems to be invalid"); - return; - } - const updatePromises = updateRecordsToBeAdded.map((updateItem) => - BuildingReusable.updateOne( - { _id: updateItem.updateId }, - { - $set: updateItem.set, - $push: { updateRecord: updateItem.updateValue }, - } - ).exec() - ); - Promise.all(updatePromises) - .then((results) => { - res.status(200).send({ - result: `Successfully posted log for ${results.length} Reusable records.`, - }); - }) - .catch((error) => res.status(500).send(error)); - } catch (err) { - res.json(err); - } - }; - - - - return { fetchBMReusables, purchaseReusable, - bmPostReusableUpdateRecord, - bmPostReusableUpdateBulk, }; }; diff --git a/src/controllers/bmdashboard/bmToolController.js b/src/controllers/bmdashboard/bmToolController.js index be37639ac..c620255b7 100644 --- a/src/controllers/bmdashboard/bmToolController.js +++ b/src/controllers/bmdashboard/bmToolController.js @@ -1,61 +1,6 @@ const mongoose = require('mongoose'); -const bmToolController = (BuildingTool, ToolType) => { - - const fetchAllTools = (req, res) => { - const populateFields = [ - { - path: 'project', - select: '_id name', - }, - { - path: 'itemType', - select: '_id name description unit imageUrl category available using', - }, - { - path: 'updateRecord', - populate: { - path: 'createdBy', - select: '_id firstName lastName', - }, - }, - { - path: 'purchaseRecord', - populate: { - path: 'requestedBy', - select: '_id firstName lastName', - }, - }, - { - path: 'logRecord', - populate: [ - { - path: 'createdBy', - select: '_id firstName lastName', - }, - { - path: 'responsibleUser', - select: '_id firstName lastName', - }, - ], - }, - ]; - - BuildingTool.find() - .populate(populateFields) - .exec() - .then(results => { - res.status(200).send(results); - }) - .catch(error => { - const errorMessage = `Error occurred while fetching tools: ${error.message}`; - console.error(errorMessage); - res.status(500).send({ message: errorMessage }); - }); - }; - - - +const bmToolController = (BuildingTool) => { const fetchSingleTool = async (req, res) => { const { toolId } = req.params; try { @@ -156,93 +101,9 @@ const bmToolController = (BuildingTool, ToolType) => { } }; - const bmLogTools = async function (req, res) { - const requestor = req.body.requestor.requestorId; - const {typesArray, action, date} = req.body - const results = []; - const errors = []; - - if(typesArray.length === 0 || typesArray === undefined){ - errors.push({ message: 'Invalid request. No tools selected'}) - return res.status(500).send({errors, results}); - } - - for (const type of typesArray) { - const toolName = type.toolName; - const toolCodes = type.toolCodes; - const codeMap = {}; - toolCodes.forEach(obj => { - codeMap[obj.value] = obj.label; - }) - - try{ - const toolTypeDoc = await ToolType.findOne({ _id: mongoose.Types.ObjectId(type.toolType) }); - if(!toolTypeDoc) { - errors.push({ message: `Tool type ${toolName} with id ${type.toolType} was not found.`}); - continue; - } - const availableItems = toolTypeDoc.available; - const usingItems = toolTypeDoc.using; - - for(const toolItem of type.toolItems){ - const buildingToolDoc = await BuildingTool.findOne({ _id: mongoose.Types.ObjectId(toolItem)}); - if(!buildingToolDoc){ - errors.push({ message: `${toolName} with id ${toolItem} was not found.`}); - continue; - } - - if(action === "Check Out" && availableItems.length > 0){ - const foundIndex = availableItems.indexOf(toolItem); - if(foundIndex >= 0){ - availableItems.splice(foundIndex, 1); - usingItems.push(toolItem); - }else{ - errors.push({ message: `${toolName} with code ${codeMap[toolItem]} is not available for ${action}`}); - continue; - } - } - - if(action === "Check In" && usingItems.length > 0){ - const foundIndex = usingItems.indexOf(toolItem); - if(foundIndex >= 0){ - usingItems.splice(foundIndex, 1); - availableItems.push(toolItem); - }else{ - errors.push({ message: `${toolName} ${codeMap[toolItem]} is not available for ${action}`}); - continue; - } - } - - const newRecord = { - date: date, - createdBy: requestor, - responsibleUser: buildingToolDoc.userResponsible, - type: action - } - - buildingToolDoc.logRecord.push(newRecord); - buildingToolDoc.save(); - results.push({message: `${action} successful for ${toolName} ${codeMap[toolItem]}`}) - } - - await toolTypeDoc.save(); - }catch(error){ - errors.push({message: `Error for tool type ${type}: ${error.message}` }); - } - } - - if (errors.length > 0) { - return res.status(404).send({ errors, results }); - } else { - return res.status(200).send({ errors, results }); - } - } - return { - fetchAllTools, fetchSingleTool, bmPurchaseTools, - bmLogTools }; }; diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index 455bf3fc2..b1dc150f4 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -2,13 +2,12 @@ const path = require("path"); const fs = require("fs/promises"); const mongoose = require("mongoose"); -const dashboardHelperClosure = require("../helpers/dashboardhelper"); +const dashboardhelper = require("../helpers/dashboardhelper")(); const emailSender = require("../utilities/emailSender"); const AIPrompt = require("../models/weeklySummaryAIPrompt"); const User = require("../models/userProfile"); const dashboardcontroller = function () { - const dashboardhelper = dashboardHelperClosure(); const dashboarddata = function (req, res) { const userId = mongoose.Types.ObjectId(req.params.userId); @@ -348,4 +347,3 @@ const dashboardcontroller = function () { }; module.exports = dashboardcontroller; - diff --git a/src/controllers/dashBoardController.spec.js b/src/controllers/dashBoardController.spec.js deleted file mode 100644 index b424eafe7..000000000 --- a/src/controllers/dashBoardController.spec.js +++ /dev/null @@ -1,838 +0,0 @@ -// const mongoose = require('mongoose'); -const AIPrompt = require('../models/weeklySummaryAIPrompt'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const UserProfile = require('../models/userProfile'); - -jest.mock('../utilities/emailSender'); -const emailSender = require('../utilities/emailSender'); - -jest.mock('../helpers/dashboardhelper'); -const dashboardHelperClosure = require('../helpers/dashboardhelper'); -const dashBoardController = require('./dashBoardController'); - -// mock the cache function before importing so we can manipulate the implementation -// jest.mock('../utilities/nodeCache'); -// const cache = require('../utilities/nodeCache'); -const makeSut = () => { - const { - updateCopiedPrompt, - getPromptCopiedDate, - updateAIPrompt, - getAIPrompt, - monthlydata, - weeklydata, - leaderboarddata, - orgData, - dashboarddata, - sendBugReport, - sendMakeSuggestion, - getSuggestionOption, - editSuggestionOption - } = dashBoardController(AIPrompt); - return { - updateCopiedPrompt, - getPromptCopiedDate, - updateAIPrompt, - getAIPrompt, - monthlydata, - weeklydata, - leaderboarddata, - orgData, - dashboarddata, - sendBugReport, - sendMakeSuggestion, - getSuggestionOption, - editSuggestionOption - }; -}; - - -const flushPromises = async () => new Promise(setImmediate); - -describe('Dashboard Controller tests', () => { - beforeAll(() => { - - }); - beforeEach(() => { - // dashboardhelper = dashboardHelperClosure(); - }); - afterEach(() => { - jest.clearAllMocks(); - }); - const error = new Error('any error'); - - - describe('updateCopiedPrompt Tests', () => { - test('Returns error 500 if the error occurs in the file update function', async () => { - - const { updateCopiedPrompt } = makeSut(); - - jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => - Promise.reject(new Error('Error Occured in the findOneAndUpdate function')), - ); - - const response = await updateCopiedPrompt(mockReq, mockRes); - - assertResMock( - 500, - new Error('Error Occured in the findOneAndUpdate function'), - response, - mockRes, - ); - }); - - test('Returns error 404 if the user is not found', async () => { - - const { updateCopiedPrompt } = makeSut(); - - jest. - spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => - Promise.resolve(null) - ); - - const response = await updateCopiedPrompt(mockReq, mockRes); - - assertResMock( - 404, - { message: "User not found " }, - response, - mockRes, - ); - }); - - test('Returns 200 if there is no error and user is found', async () => { - - const { updateCopiedPrompt } = makeSut(); - - jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => - Promise.resolve("Copied AI prompt") - ); - - const response = await updateCopiedPrompt(mockReq, mockRes); - - assertResMock( - 200, - "Copied AI prompt", - response, - mockRes, - ); - }) - - }); - - describe('getPromptCopiedDate', () => { - test('Returns 200 if there is a user and return copied AI prompt',async () => { - const mockUser = { _id: 'testUserId', copiedAiPrompt: 'Test Prompt'}; - - const newReq = { - ...mockReq, - params: { - userId: 'testUserId' - } - }; - - const { getPromptCopiedDate } = makeSut(); - - jest - .spyOn(UserProfile, 'findOne') - .mockResolvedValueOnce(mockUser); - - await getPromptCopiedDate(newReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith({ message: mockUser.copiedAiPrompt }); - }); - - test('Returns undefined when the user is not found', async () => { - - const { getPromptCopiedDate } = makeSut(); - - jest - .spyOn(UserProfile, 'findOne') - .mockResolvedValueOnce(null); - - getPromptCopiedDate(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).not.toHaveBeenCalled(); - expect(mockRes.send).not.toHaveBeenCalled(); - }) - }) - - describe('updateAIPrompt Tests', () => { - test('Returns error 500 if the error occurs in the AI Prompt function', async () => { - const newRequest = { - ...mockReq, - body: { - requestor: { - role: 'Owner' - } - } - }; - - const { updateAIPrompt } = makeSut(); - - jest - .spyOn(AIPrompt, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.reject(error)); - - const response = updateAIPrompt(newRequest, mockRes); - - await flushPromises(); - - assertResMock( - 500, - error, - response, - mockRes, - ); - }); - - test('Returns 200 if there is no error and AI Prompt is saved', async () => { - const newRequest = { - ...mockReq, - body: { - requestor: { - role: 'Owner' - } - } - }; - - const { updateAIPrompt } = makeSut(); - - jest - .spyOn(AIPrompt, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve("Successfully saved AI prompt.")); - - const response = updateAIPrompt(newRequest, mockRes); - - await flushPromises(); - - assertResMock( - 200, - "Successfully saved AI prompt.", - response, - mockRes, - ); - }); - - test('Returns undefined if requestor role is not an owner', () => { - const newRequest = { - ...mockReq, - body: { - requestor: { - role: 'Administrator' - } - } - }; - const { updateAIPrompt } = makeSut(); - - const mockFindOneAndUpdate = jest - .spyOn(AIPrompt, 'findOneAndUpdate') - .mockImplementationOnce(() => - Promise.resolve({undefined}), - ); - - const response = updateAIPrompt(newRequest, mockRes); - - expect(response).toBeUndefined(); - expect(mockRes.status).not.toHaveBeenCalled(); - expect(mockRes.send).not.toHaveBeenCalled(); - expect(mockFindOneAndUpdate).not.toHaveBeenCalled(); - }); - - }); - - describe('getAIPrompt Tests', () => { - - test('Returns 200 if the GPT exists and send the results back', async () => { - - const { getAIPrompt } = makeSut(); - - jest - .spyOn(AIPrompt,'findById') - .mockImplementationOnce(() => Promise.resolve({})) - - const response = getAIPrompt(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - {}, - response, - mockRes, - ) - }); - - test('Returns 200 if there is no error and new GPT Prompt is created', async () => { - - const { getAIPrompt } = makeSut(); - - jest - .spyOn(AIPrompt, 'findById') - .mockResolvedValueOnce(null); - - jest - .spyOn(AIPrompt, 'create') - .mockImplementationOnce(() => Promise.resolve({})); - - const response = getAIPrompt(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - {}, - response, - mockRes, - ) - }); - - test('Returns 500 if GPT Prompt does not exist', async () => { - - const { getAIPrompt } = makeSut(); - const errorMessage = 'GPT Prompt does not exist'; - - jest - .spyOn(AIPrompt, 'findById') - .mockRejectedValueOnce(new Error(errorMessage)); - - const response = getAIPrompt(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 500, - new Error(errorMessage), - response, - mockRes, - ); - }); - - test('Returns 500 if there is an error in creating the GPT Prompt', async () => { - - const { getAIPrompt } = makeSut(); - const errorMessage = 'Error in creating the GPT Prompt'; - - jest - .spyOn(AIPrompt, 'findById') - .mockResolvedValueOnce(null); - - jest - .spyOn(AIPrompt, 'create') - .mockRejectedValueOnce(new Error(errorMessage)); - - const response = getAIPrompt(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 500, - new Error(errorMessage), - response, - mockRes, - ); - }); - - }); - - describe('weeklydata Tests', () => { - - test('Returns 200 if there is no error and labordata is found', async () => { - const dashboardHelperObject = - { - laborthisweek: jest.fn(() => Promise.resolve([])) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { weeklydata } = makeSut(); - - const response = weeklydata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - [], - response, - mockRes, - ); - }) - }); - - describe('monthlydata Tests', () => { - - test('Returns 200 if there is no results and return empty results', async () => { - const dashboardHelperObject = { - laborthismonth: jest.fn(() => Promise.resolve([{ - projectName: "", - timeSpent_hrs: 0, - }])) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { monthlydata } = makeSut(); - - const response = monthlydata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - [{ - projectName: "", - timeSpent_hrs: 0, - }], - response, - mockRes, - ); - }) - - test('Returns 200 if there is results and return results', async () => { - const dashboardHelperObject = { - laborthismonth: jest.fn(() => Promise.resolve({})) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { monthlydata } = makeSut(); - - const response = monthlydata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - {}, - response, - mockRes, - ); - }) - - }); - - describe('leaderboarddata Tests', () => { - test('Returns 200 if there is leaderboard data', async () => { - const dashboardHelperObject = { - getLeaderboard: jest.fn(() => Promise.resolve({})), - getUserLaborData: jest.fn(() => Promise.resolve({})) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { leaderboarddata } = makeSut(); - - const response = leaderboarddata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - {}, - response, - mockRes, - ); - }) - - test('Returns 200 if leaderboard data is empty and returns getUserLaborData', async () => { - const dashboardHelperObject = { - getLeaderboard: jest.fn(() => Promise.resolve([])), - getUserLaborData: jest.fn(() => Promise.resolve([])) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { leaderboarddata } = makeSut(); - - const response = leaderboarddata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - [], - response, - mockRes, - ); - }) - - test('Returns 400 if there is an error', async () => { - const dashboardHelperObject = { - getLeaderboard: jest.fn(() => Promise.reject({})) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { leaderboarddata } = makeSut(); - - const response = leaderboarddata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 400, - {}, - response, - mockRes, - ); - }) - }) - - describe('orgData Tests', () => { - - test('Returns 400 if there is an error in the function', async () => { - - const dashboardHelperObject = { - getOrgData: jest.fn(() => Promise.reject(error)) - }; - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { orgData } = makeSut(); - - const response = orgData(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 400, - error, - response, - mockRes, - ); - }) - - test('Returns 200 if the result is found and returns result', async () => { - const mockResult = { id: 1, name: 'Mock Results'}; - - const dashboardHelperObject = { - getOrgData: jest.fn(() => Promise.resolve([mockResult])) - } - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { orgData } = makeSut(); - - const response = orgData(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - mockResult, - response, - mockRes, - ); - }) - }); - - describe('dashboarddata Tests', () => { - test('Returns 200 if there is no error and return results', async () => { - - const dashboardHelperObject = { - personaldetails: jest.fn(() => Promise.resolve({})) - } - - dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); - - const { dashboarddata } = makeSut(); - - const response = dashboarddata(mockReq, mockRes); - - await flushPromises(); - - assertResMock( - 200, - {}, - response, - mockRes, - ) - }) - - }); - - describe('sendBugReport Tests', () => { - - test('Returns 200 if the bug report email is sent ', async () => { - - mockReq.body = { - ...mockReq.body, - firstName: 'Lin', - lastName: 'Test', - title: 'Bug in feature X', - environment: 'macOS 10.15, Chrome 89, App version 1.2.3', - reproduction: '1. Click on button A\n2. Enter valid data\n3. Click submit', - expected: 'The app should not display an error message', - actual: 'The app', - visual: 'Screenshot attached', - severity: 'High', - email: 'lin.test@example.com', - }; - - const { sendBugReport } = makeSut(); - - sendBugReport(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith('Success'); - }) - - test('Returns 500 if the email fails to send', async () => { - - mockReq.body = { - ...mockReq.body, - firstName: 'Lin', - lastName: 'Test', - title: 'Bug in feature X', - environment: 'macOS 10.15, Chrome 89, App version 1.2.3', - reproduction: '1. Click on button A\n2. Enter valid data\n3. Click submit', - expected: 'The app should not display an error message', - actual: 'The app', - visual: 'Screenshot attached', - severity: 'High', - email: 'lin.test@example.com', - }; - - emailSender.mockImplementation(() => { - throw new Error('Failed to send email'); - }); - - const { sendBugReport } = makeSut(); - - sendBugReport(mockReq, mockRes); - - emailSender.mockRejectedValue(new Error('Failed')); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith('Failed'); - }) - - }) - - describe('sendMakeSuggestion Tests', () => { - test('Returns 500 if the suggestion email fails to send', async () => { - - mockReq.body = { - suggestioncate: 'Identify and remedy poor client and/or user service experiences', - suggestion: 'This is a sample suggestion', - confirm: 'true', - email: 'test@example.com', - firstName: 'Lin', - lastName: 'Test', - field: ['field1', 'field2'], - }; - - emailSender.mockImplementation(() => { - throw new Error('Failed'); - }); - - const { sendMakeSuggestion } = makeSut(); - - sendMakeSuggestion(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith('Failed'); - }) - - test('Returns 200 if the suggestion email is sent successfully', async () => { - - mockReq.body = { - ...mockReq.body, - suggestioncate: 'Identify and remedy poor client and/or user service experiences', - suggestion: 'This is a sample suggestion', - confirm: 'true', - email: 'john.doe@example.com', - firstName: 'John', - lastName: 'Doe', - field: ['field1', 'field2'], - }; - - emailSender.mockImplementation(() => { - Promise.resolve(); - }); - - const { sendMakeSuggestion } = makeSut(); - - sendMakeSuggestion(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith('Success'); - }) - - }) - - // Need to make test cases for negative case - describe('getSuggestionOption Tests', () => { - - // test.only('Returns 404 if the suggestion data is not found', async () => { - - // const { getSuggestionOption } = makeSut(); - - // await getSuggestionOption(mockReq, mockRes); - - // await flushPromises(); - - // expect(mockRes.status).toHaveBeenCalledWith(404); - // expect(mockRes.send).toHaveBeenCalledWith('Suggestion Data Not Found'); - // }); - - test('Returns 200 if there is suggestion data', async () => { - - const suggestionData = { - "field": [], - "suggestion": [ - "Identify and remedy poor client and/or user service experiences", - "Identify bright spots and enhance positive service experiences", - "Make fundamental changes to our programs and/or operations", - "Inform the development of new programs/projects", - "Identify where we are less inclusive or equitable across demographic groups", - "Strengthen relationships with the people we serve", - "Understand people's needs and how we can help them achieve their goals", - "Other" - ] - }; - - const { getSuggestionOption } = makeSut(); - - await getSuggestionOption(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith(suggestionData); - }) - }) - - // Need to make test cases for negative case - describe('editSuggestionOption tests', () => { - test('Returns 200 if suggestionData.field is added a new field', async () => { - - const suggestionData = { - suggestion: ['newSuggestion'], - field: ['newField'], - }; - - mockReq.body = { - suggestion: true, - action: 'add', - newField: 'new field', - }; - - const { editSuggestionOption } = makeSut(); - - await editSuggestionOption(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(suggestionData.field).toEqual(['newField']); - expect(mockRes.send).toHaveBeenCalledWith('success'); - }); - - test('Returns 200 if suggestionData.suggestion is added a new suggestion', async () => { - - const suggestionData = { - suggestion: ['newSuggestion'], - field: [], - }; - - mockReq.body = { - suggestion: true, - action: 'add', - newField: 'new suggestion', - }; - - const { editSuggestionOption } = makeSut(); - - await editSuggestionOption(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(suggestionData.suggestion).toEqual(['newSuggestion']); - expect(mockRes.send).toHaveBeenCalledWith('success'); - }) - - test('Returns 200 if suggestionData.field is deleted', async () => { - - const suggestionData = { - suggestion: ['newSuggestion'], - field: [], - }; - - mockReq.body = { - suggestion: true, - action: 'delete', - newField: 'new field', - }; - - const { editSuggestionOption } = makeSut(); - - await editSuggestionOption(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(suggestionData.field).toEqual([]); - expect(mockRes.send).toHaveBeenCalledWith('success'); - }); - - test('Returns 200 if suggestionData.suggestion is deleted', async () => { - - const suggestionData = { - suggestion: [], - field: [], - }; - - mockReq.body = { - suggestion: true, - action: 'delete', - newField: 'new field', - }; - - const { editSuggestionOption } = makeSut(); - - await editSuggestionOption(mockReq, mockRes); - - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(suggestionData.suggestion).toEqual([]); - expect(mockRes.send).toHaveBeenCalledWith('success'); - }); - - // test.only('Returns 500 if there is an error in the function', async () => { - - // const { editSuggestionOption } = makeSut(); - - // await editSuggestionOption(mockReq, mockRes); - - // jest - // .spyOn(console, 'error') - // .mockRejectedValueOnce('Internal Server Error') - - // expect(mockRes.status).toHaveBeenCalledWith(500); - // expect(mockRes.send).toHaveBeenCalledWith('Internal Server Error'); - // }); - - - }) - -}); diff --git a/src/controllers/forcePwdController.spec.js b/src/controllers/forcePwdController.spec.js deleted file mode 100644 index a6d02d381..000000000 --- a/src/controllers/forcePwdController.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -const forcePwdcontroller = require('./forcePwdController'); -const userProfile = require('../models/userProfile'); -const { mockReq, mockRes, assertResMock } = require('../test'); - -const makeSut = () => { - const { forcePwd } = forcePwdcontroller(userProfile); - - return { - forcePwd, - }; -}; - -const flushPromises = () => new Promise(setImmediate); - -describe('ForcePwdController Unit Tests', () => { - beforeEach(() => { - mockReq.body.userId = '65cf6c3706d8ac105827bb2e'; - mockReq.body.newpassword = 'newPasswordReset'; - - }); - afterEach(() => { - jest.clearAllMocks(); - }); - - test('Returns a 400 bad request status if userId is not valid with an error message Bad Request', async () => { - const { forcePwd } = makeSut(); - const errorMsg = { error: 'Bad Request' }; - mockReq.body.userId = ''; - const response = await forcePwd(mockReq, mockRes); - assertResMock(400, errorMsg, response, mockRes); - }); - test('Returns a 500 Internal Error if finding userProfile throws an error', async () => { - const { forcePwd } = makeSut(); - const errorMsg = 'Error happened when finding user'; - jest.spyOn(userProfile, 'findById').mockImplementationOnce(() => Promise.reject(errorMsg)); - const response = forcePwd(mockReq, mockRes); - await flushPromises(); - assertResMock(500, errorMsg, response, mockRes); - }); - test('Returns a 200 OK status with a success message "password Reset"', async () => { - const { forcePwd } = makeSut(); - const successMsg = { message: ' password Reset' }; - const mockUser = { - set: jest.fn(), - save: jest.fn().mockResolvedValue({}), - }; - - jest.spyOn(userProfile, 'findById').mockResolvedValue(mockUser); - - const response = forcePwd(mockReq, mockRes); - await flushPromises(); - assertResMock(200, successMsg, response, mockRes); - }); - test('Returns a 500 Internal Error status if new password fails to save', async () => { - const { forcePwd } = makeSut(); - const errorMsg = 'Error happened when saving user'; - const mockUser = { - set: jest.fn(), - save: jest.fn().mockRejectedValue(errorMsg), - }; - - jest.spyOn(userProfile, 'findById').mockResolvedValue(mockUser); - - const response = forcePwd(mockReq, mockRes); - await flushPromises(); - assertResMock(500, errorMsg, response, mockRes); - }); -}); diff --git a/src/controllers/forgotPwdcontroller.spec.js b/src/controllers/forgotPwdcontroller.spec.js deleted file mode 100644 index f8e1f2a6b..000000000 --- a/src/controllers/forgotPwdcontroller.spec.js +++ /dev/null @@ -1,183 +0,0 @@ -jest.mock('uuid/v4'); -jest.mock('../utilities/emailSender'); - -const uuidv4 = require('uuid/v4'); -const emailSender = require('../utilities/emailSender'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const forgotPwdController = require('./forgotPwdcontroller'); -const UserProfile = require('../models/userProfile'); -const escapeRegex = require('../utilities/escapeRegex'); - -uuidv4.mockReturnValue(''); -emailSender.mockImplementation(() => undefined); - -const flushPromises = () => new Promise(setImmediate); - -// Positive -// ✅ Return 200 if successfully generated temporary User password. - -// Negative -// ✅ Return 500 if any error encountered while fetching User details. -// ✅ Return 500 if any error encountered while saving User's password. -// ✅ Return 400 if valid user not found. - -function getEmailMessageForForgotPassword(user, ranPwd) { - const message = ` Hello ${user.firstName} ${user.lastName}, -

Congratulations on successfully completing the Highest Good Network 3-question Change My Password Challenge. Your reward is this NEW PASSWORD!

-
${ranPwd}
-

Use it now to log in. Then store it in a safe place or change it on your Profile Page to something easier for you to remember.

-

If it wasn’t you that requested this password change, you can ignore this email. Otherwise, use the password above to log in and you’ll be directed to the “Change Password” page where you can set a new custom one.

-

Thank you,

-

One Community

`; - return message; -} - -const makeSut = () => { - const { forgotPwd } = forgotPwdController(UserProfile); - return { forgotPwd }; -}; - -describe('Unit Tests for forgotPwdcontroller.js', () => { - beforeAll(() => { - mockReq.body.email = 'parthgrads@gmail.com'; - mockReq.body.firstName = 'Parth'; - mockReq.body.lastName = 'Jangid'; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('Forgot Pwd Function', () => { - test('Returns 500 if any error encountered while fetching user.', async () => { - const { forgotPwd } = makeSut(); - - const error = new Error('Database error'); - const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockRejectedValueOnce(error); - - const response = await forgotPwd(mockReq, mockRes); - - assertResMock(500, error, response, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ - // Check Parameters to findOne - email: { - $regex: escapeRegex(mockReq.body.email), - $options: 'i', - }, - firstName: { - $regex: escapeRegex(mockReq.body.firstName), - $options: 'i', - }, - lastName: { - $regex: escapeRegex(mockReq.body.lastName), - $options: 'i', - }, - }); - }); - - test('Returns 400 if No Valid User found', async () => { - const { forgotPwd } = makeSut(); - - const userObject = null; // or undefined - const error = { error: 'No Valid user was found' }; - - const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(userObject); - - const response = await forgotPwd(mockReq, mockRes); - - assertResMock(400, error, response, mockRes); - - expect(findOneSpy).toHaveBeenCalledWith({ - email: { - $regex: escapeRegex(mockReq.body.email), - $options: 'i', - }, - firstName: { - $regex: escapeRegex(mockReq.body.firstName), - $options: 'i', - }, - lastName: { - $regex: escapeRegex(mockReq.body.lastName), - $options: 'i', - }, - }); - }); - - test('Return 500 if encountered any error while saving temporary password', async () => { - const { forgotPwd } = makeSut(); - - const error = new Error('Error Saving User Details'); - - const mockUser = { - set: jest.fn(), // Mocking the set method - save: jest.fn().mockRejectedValueOnce(error), // Mocked below using spyOn - }; - - const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(mockUser); - - const response = await forgotPwd(mockReq, mockRes); - await flushPromises(); - expect(mockUser.set).toHaveBeenCalled(); - expect(mockUser.save).toHaveBeenCalled(); - assertResMock(500, error, response, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ - email: { - $regex: escapeRegex(mockReq.body.email), - $options: 'i', - }, - firstName: { - $regex: escapeRegex(mockReq.body.firstName), - $options: 'i', - }, - lastName: { - $regex: escapeRegex(mockReq.body.lastName), - $options: 'i', - }, - }); - }); - - test('Return 200 if a temporary password is generated for the user', async () => { - const { forgotPwd } = makeSut(); - - const mockUser = { - // denote the User object obtained by find operation on MongoDB - email: mockReq.body.email, - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - set: jest.fn(), // Mocking the set method - save: jest.fn().mockResolvedValueOnce(), // Mocking the save method - }; - - const message = { message: 'generated new password' }; - const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(mockUser); - - const response = await forgotPwd(mockReq, mockRes); - const temporaryPassword = uuidv4().concat('TEMP'); // The source code appends "TEMP" so does this line - - expect(mockUser.set).toHaveBeenCalled(); - expect(mockUser.save).toHaveBeenCalled(); - expect(emailSender).toHaveBeenCalledWith( - mockUser.email, - 'Account Password change', - getEmailMessageForForgotPassword(mockUser, temporaryPassword), - null, - null, - ); - assertResMock(200, message, response, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ - email: { - $regex: escapeRegex(mockReq.body.email), - $options: 'i', - }, - firstName: { - $regex: escapeRegex(mockReq.body.firstName), - $options: 'i', - }, - lastName: { - $regex: escapeRegex(mockReq.body.lastName), - $options: 'i', - }, - }); - }); - }); -}); diff --git a/src/controllers/inventoryController.js b/src/controllers/inventoryController.js index d126cc7e4..14082bbc8 100644 --- a/src/controllers/inventoryController.js +++ b/src/controllers/inventoryController.js @@ -13,7 +13,7 @@ const inventoryController = function (Item, ItemType) { // use req.params.projectId and wbsId // Run a mongo query on the Item model to find all items with both the project and wbs // sort the mongo query so that the Wasted false items are listed first - return Item.find({ + await Item.find({ project: mongoose.Types.ObjectId(req.params.projectId), wbs: req.params.wbsId && req.params.wbsId !== 'Unassigned' @@ -283,9 +283,9 @@ const inventoryController = function (Item, ItemType) { } // update the original item by decreasing by the quantity and adding a note - if (req.body.quantity && req.params.invId && projectExists && wbsExists) { + if (req.body.quantity && req.param.invId && projectExists && wbsExists) { return Item.findByIdAndUpdate( - req.params.invId, + req.param.invId, { $decr: { quantity: req.body.quantity }, $push: { diff --git a/src/controllers/inventoryController.spec.js b/src/controllers/inventoryController.spec.js new file mode 100644 index 000000000..43f7f3682 --- /dev/null +++ b/src/controllers/inventoryController.spec.js @@ -0,0 +1,334 @@ +/* eslint-disable new-cap */ + +jest.mock('../utilities/permissions', () => ({ + hasPermission: jest.fn(), // Mocking the hasPermission function +})); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const inventoryItem = require('../models/inventoryItem'); +const inventoryItemType = require('../models/inventoryItemType'); +const inventoryController = require('./inventoryController'); +const projects = require('../models/project'); +const wbs = require('../models/wbs'); + +const { hasPermission } = require('../utilities/permissions'); + +const makeSut = () => { + const { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject } = inventoryController( + inventoryItem, + inventoryItemType, + ); + return { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject }; +}; + +const flushPromises = () => new Promise(setImmediate); + +describe('Unit test for inventoryController', () => { + beforeAll(() => { + jest.clearAllMocks(); + }); + beforeEach(() => { + mockReq.params.userid = '5a7e21f00317bc1538def4b7'; + mockReq.params.userId = '5a7e21f00317bc1538def4b7'; + mockReq.params.wbsId = '5a7e21f00317bc1538def4b7'; + mockReq.params.projectId = '5a7e21f00317bc1538def4b7'; + mockReq.body = { + project: '5a7e21f00317bc1538def4b7', + wbs: '5a7e21f00317bc1538def4b7', + itemType: '5a7e21f00317bc1538def4b7', + item: '5a7e21f00317bc1538def4b7', + quantity: 1, + typeId: '5a7e21f00317bc1538def4b7', + cost: 20, + poNum: '123', + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getAllInvInProjectWBS', () => { + test('Returns 403 if user is not authorized to view inventory data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockResolvedValue(false); + const response = await getAllInvInProjectWBS(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 404 if an error occurs while fetching inventory data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + // Mocking hasPermission function + hasPermission.mockResolvedValue(true); + + // Mock error + const error = new Error('Error fetching inventory data'); + + // Mock chainable methods: populate, sort, then, catch + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), + catch: jest.fn().mockReturnThis(), + }; + + // Mock the inventoryItem.find method + jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); + + // Call the function + const response = await getAllInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + // Assertions + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 if successfully found data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockResolvedValue(true); + + const mockData = [ + { + _id: '123', + project: '123', + wbs: '123', + itemType: '123', + item: '123', + quantity: 1, + date: new Date().toISOString(), + }, + ]; + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockResolvedValue(mockData), + then: jest.fn().mockResolvedValue(() => {}), + catch: jest.fn().mockReturnThis(), + }; + + // Mock the inventoryItem.find method + jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); + + // Call the function + const response = await getAllInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + // Assertions + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(200, mockData, response, mockRes); + }); + }); + describe('postInvInProjectWBS', () => { + test('Returns error 403 if the user is not authorized to view data', async () => { + const { getAllInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(false); + const response = await getAllInvInProjectWBS(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns error 400 if an error occurs while fetching an item', async () => { + mockReq.params.wbsId = 'Unassigned'; + const { postInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(true); + // look up difference betewewen mockimplmenonce and mockimplementation + // how to incorpoate into the test + // and how to setup mocking variables as well + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock( + 400, + 'Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned', + response, + mockRes, + ); + }); + test('Returns error 500 if an error occurs when saving', async () => { + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryItem = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + const { postInvInProjectWBS } = makeSut(); + // const hasPermissionSpy = mockHasPermission(true); + hasPermission.mockReturnValue(true); + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); + + jest.spyOn(inventoryItem.prototype, 'save').mockRejectedValueOnce(new Error('Error saving')); + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(500, new Error('Error saving'), response, mockRes); + }); + + test('Receives a 201 success if the inventory was successfully created and saved', async () => { + const resolvedInventoryItem = new inventoryItem({ + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity, + cost: mockReq.body.cost, + poNum: mockReq.body.poNum, + }); + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryItem = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnValue(null), + }; + const { postInvInProjectWBS } = makeSut(); + + hasPermission.mockReturnValue(true); + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); + jest + .spyOn(inventoryItem.prototype, 'save') + .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(201, resolvedInventoryItem, response, mockRes); + }); + + test('Returns a 201, if the inventory item was succesfully updated and saved.', async () => { + const resolvedInventoryItem = { + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity, + cost: mockReq.body.cost, + poNum: mockReq.body.poNum, + }; + + const updatedResolvedInventoryItem = { + project: mockReq.body.projectId, + wbs: mockReq.body.wbsId, + type: mockReq.body.typeId, + quantity: mockReq.body.quantity + 1, + costPer: 200, + }; + + const mockProjectExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockWbsExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + const mockInventoryExists = { + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + }; + + const { postInvInProjectWBS } = makeSut(); + hasPermission.mockReturnValue(true); + + jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); + jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); + jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryExists); + jest + .spyOn(inventoryItem, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); + + jest + .spyOn(inventoryItem, 'findByIdAndUpdate') + .mockImplementationOnce(() => Promise.resolve(updatedResolvedInventoryItem)); + + const response = await postInvInProjectWBS(mockReq, mockRes); + await flushPromises(); + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(201, updatedResolvedInventoryItem, response, mockRes); + }); + }); + + describe('getAllInvInProject', () => { + test('Returns 403 if user is not authorized to view inventory data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(false); + const response = await getAllInvInProject(mockReq, mockRes); + assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 404 if an error occurs while fetching inventory data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(true); + + const error = new Error('Error fetching inventory data'); + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), + catch: jest.fn().mockReturnThis(), + }; + + jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); + + const response = await getAllInvInProject(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(404, error, response, mockRes); + }); + + test('Returns 200 if successfully found data', async () => { + const { getAllInvInProject } = makeSut(); + hasPermission.mockResolvedValue(true); + + const mockData = [ + { + _id: '123', + project: '123', + wbs: '123', + itemType: '123', + item: '123', + quantity: 1, + date: new Date().toISOString(), + }, + ]; + + const mockInventoryItem = { + populate: jest.fn().mockReturnThis(), + sort: jest.fn().mockResolvedValue(mockData), + catch: jest.fn().mockReturnThis(), + }; + + jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); + + const response = await getAllInvInProject(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toHaveBeenCalledTimes(1); + assertResMock(200, mockData, response, mockRes); + }); + }); +}); diff --git a/src/controllers/logincontroller.js b/src/controllers/logincontroller.js index 3ba0203aa..809c5892f 100644 --- a/src/controllers/logincontroller.js +++ b/src/controllers/logincontroller.js @@ -23,10 +23,7 @@ const logincontroller = function () { if (!user) { res.status(403).send({ message: 'Username not found.' }); } else if (user.isActive === false) { - res.status(403).send({ - message: - 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.', - }); + res.status(403).send({ message: 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.' }); } else { let isPasswordMatch = false; let isNewUser = false; @@ -37,42 +34,42 @@ const logincontroller = function () { isPasswordMatch = await bcrypt.compare(_password, user.password); if (!isPasswordMatch && user.resetPwd !== '') { - isPasswordMatch = _password === user.resetPwd; + isPasswordMatch = (_password === user.resetPwd); isNewUser = true; } - if (isNewUser && isPasswordMatch) { - const result = { - new: true, - userId: user._id, - }; - res.status(200).send(result); - } else if (isPasswordMatch && !isNewUser) { - const jwtPayload = { - userid: user._id, - role: user.role, - permissions: user.permissions, - access: { - canAccessBMPortal: false, - }, - email: user.email, - expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), - }; + if (isNewUser && isPasswordMatch) { + const result = { + new: true, + userId: user._id, + }; + res.status(200).send(result); + } else if (isPasswordMatch && !isNewUser) { + const jwtPayload = { + userid: user._id, + role: user.role, + permissions: user.permissions, + access: { + canAccessBMPortal: false, + }, + email: user.email, + expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), + }; - const token = jwt.sign(jwtPayload, JWT_SECRET); + const token = jwt.sign(jwtPayload, JWT_SECRET); - res.status(200).send({ token }); - } else { - res.status(403).send({ - message: 'Invalid password.', - }); - } + res.status(200).send({ token }); + } else { + res.status(403).send({ + message: 'Invalid password.', + }); } - } catch (err) { - console.log(err); - res.json(err); - } - }; + } + } catch (err) { + console.log(err); + res.json(err); + } +}; const getUser = function (req, res) { const { requestor } = req.body; @@ -81,6 +78,7 @@ const logincontroller = function () { }; return { + login, getUser, }; diff --git a/src/controllers/logincontroller.spec.js b/src/controllers/logincontroller.spec.js deleted file mode 100644 index 595bfe77b..000000000 --- a/src/controllers/logincontroller.spec.js +++ /dev/null @@ -1,211 +0,0 @@ -const path = require('path'); -require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); -const bcrypt = require('bcryptjs'); -const logincontroller = require('./logincontroller'); -const { mockReq, mockRes, assertResMock, mockUser } = require('../test'); -const userProfile = require('../models/userProfile'); - -const makeSut = () => { - const { login, getUser } = logincontroller(); - return { - login, - getUser, - }; -}; - -describe('logincontroller module', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('login', () => { - test('Ensure login returns error 400 if there is no email or password', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: '', - password: '', - }, - }, - }; - const res = await login(mockReqModified, mockRes); - assertResMock(400, { error: 'Invalid request' }, res, mockRes); - }); - - test('Ensure login returns error 403 if there is no user', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: 'example@test.com', - password: 'exampletest', - }, - }, - }; - const findOneSpy = jest - .spyOn(userProfile, 'findOne') - .mockImplementation(() => Promise.resolve(null)); - - const res = await login(mockReqModified, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); - assertResMock(403, { message: 'Username not found.' }, res, mockRes); - }); - - test('Ensure login returns error 403 if the user exists but is not active', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: 'example@test.com', - password: 'exampletest', - }, - }, - }; - const mockUserModified = { - ...mockUser, - ...{ - isActive: false, - }, - }; - - const findOneSpy = jest - .spyOn(userProfile, 'findOne') - .mockImplementation(() => Promise.resolve(mockUserModified)); - - const res = await login(mockReqModified, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); - assertResMock( - 403, - { - message: - 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.', - }, - res, - mockRes, - ); - }); - - test('Ensure login returns error 403 if the password is not a match and if the user already exists', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: 'example@test.com', - password: 'SuperSecretPassword@', - }, - }, - }; - - const findOneSpy = jest - .spyOn(userProfile, 'findOne') - .mockImplementation(() => Promise.resolve(mockUser)); - jest.spyOn(bcrypt, 'compare').mockResolvedValue(false); - - const res = await login(mockReqModified, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); - - assertResMock( - 403, - { - message: 'Invalid password.', - }, - res, - mockRes, - ); - }); - - test('Ensure login returns the error if the try block fails', async () => { - const { login } = makeSut(); - const error = new Error('Try block failed'); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: 'example@test.com', - password: 'exampletest', - }, - }, - }; - - jest.spyOn(userProfile, 'findOne').mockImplementation(() => Promise.reject(error)); - - await login(mockReqModified, mockRes); - expect(mockRes.json).toHaveBeenCalledWith(error); - }); - - test('Ensure login returns 200, if the user is a new user and there is a password match', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - ...{ - body: { - email: 'example@example.com', - password: '123Welcome!', - }, - }, - }; - - const mockUserModified = { - _id: 'user123', - email: 'example@example.com', - password: 'hashedPassword', - resetPwd: 'newUserPassword', - isActive: true, - }; - - jest - .spyOn(userProfile, 'findOne') - .mockImplementation(() => Promise.resolve(mockUserModified)); - - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - - const res = await login(mockReqModified, mockRes); - assertResMock(200, { new: true, userId: 'user123' }, res, mockRes); - }); - - test('Ensure login returns 200, if the user already exists and the password is a match', async () => { - const { login } = makeSut(); - const mockReqModified = { - ...mockReq, - body: { - email: 'existing@example.com', - password: 'existingUserPassword', - }, - }; - const mockUserModified = { - _id: 'user123', - email: 'existing@example.com', - password: 'hashedPassword', - resetPwd: 'newUserPassword', - isActive: true, - role: 'Volunteer', - permissions: ['read', 'write'], - }; - - const findOneSpy = jest - .spyOn(userProfile, 'findOne') - .mockImplementation(() => Promise.resolve(mockUserModified)); - - jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); - - const res = await login(mockReqModified, mockRes); - expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); - - assertResMock(200, { token: expect.any(String) }, res, mockRes); - }); - }); - - describe('getUser', () => { - it('Ensure getUser returns 200, with the requestor body', () => { - const { getUser } = makeSut(); - - const res = getUser(mockReq, mockRes); - assertResMock(200, mockReq.body.requestor, res, mockRes); - }); - }); -}); diff --git a/src/controllers/mapLocationsController.js b/src/controllers/mapLocationsController.js index bb85fe9f3..a2c98fcf1 100644 --- a/src/controllers/mapLocationsController.js +++ b/src/controllers/mapLocationsController.js @@ -1,24 +1,28 @@ const UserProfile = require('../models/userProfile'); -const cache = require('../utilities/nodeCache')(); +const cacheClosure = require('../utilities/nodeCache'); const mapLocationsController = function (MapLocation) { + const cache = cacheClosure(); const getAllLocations = async function (req, res) { try { const users = []; const results = await UserProfile.find( -{}, + {}, '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', -); + ); results.forEach((item) => { if ( - (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) - || (item.location?.coords.lat && item.location?.coords.lng && calculateTotalHours(item.hoursByCategory) >= 10) + (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || + (item.location?.coords.lat && + item.location?.coords.lng && + // eslint-disable-next-line no-use-before-define + calculateTotalHours(item.hoursByCategory) >= 10) ) { users.push(item); } }); - const modifiedUsers = users.map(item => ({ + const modifiedUsers = users.map((item) => ({ location: item.homeCountry || item.location, isActive: item.isActive, jobTitle: item.jobTitle[0], @@ -34,7 +38,7 @@ const mapLocationsController = function (MapLocation) { } }; const deleteLocation = async function (req, res) { - if (!req.body.requestor.role === 'Administrator' || !req.body.requestor.role === 'Owner') { + if (req.body.requestor.role !== 'Administrator' && req.body.requestor.role !== 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -42,13 +46,14 @@ const mapLocationsController = function (MapLocation) { MapLocation.findOneAndDelete({ _id: locationId }) .then(() => res.status(200).send({ message: 'The location was successfully removed!' })) - .catch(error => res.status(500).send({ message: error || "Couldn't remove the location" })); + .catch((error) => res.status(500).send({ message: error || "Couldn't remove the location" })); }; const putUserLocation = async function (req, res) { - if (!req.body.requestor.role === 'Owner') { + if (req.body.requestor.role !== 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } + const locationData = { firstName: req.body.firstName, lastName: req.body.lastName, @@ -65,11 +70,11 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(response); } catch (err) { console.log(err.message); - res.status(500).json({ message: err.message || 'Something went wrong...' }); + res.status(500).send({ message: err.message || 'Something went wrong...' }); } }; const updateUserLocation = async function (req, res) { - if (!req.body.requestor.role === 'Owner') { + if (req.body.requestor.role !== 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -89,13 +94,21 @@ const mapLocationsController = function (MapLocation) { try { let response; if (userType === 'user') { - response = await UserProfile.findOneAndUpdate({ _id: userId }, { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, { new: true }); + response = await UserProfile.findOneAndUpdate( + { _id: userId }, + { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, + { new: true }, + ); cache.removeCache('allusers'); cache.removeCache(`user-${userId}`); cache.setCache(`user-${userId}`, JSON.stringify(response)); } else { - response = await MapLocation.findOneAndUpdate({ _id: userId }, { $set: updateData }, { new: true }); + response = await MapLocation.findOneAndUpdate( + { _id: userId }, + { $set: updateData }, + { new: true }, + ); } if (!response) { @@ -113,7 +126,7 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(newData); } catch (err) { console.log(err.message); - res.status(500).json({ message: err.message || 'Something went wrong...' }); + res.status(500).send({ message: err.message || 'Something went wrong...' }); } }; diff --git a/src/controllers/mapLocationsController.spec.js b/src/controllers/mapLocationsController.spec.js new file mode 100644 index 000000000..871ca1088 --- /dev/null +++ b/src/controllers/mapLocationsController.spec.js @@ -0,0 +1,367 @@ +/// mock the cache function before importing so we can manipulate the implementation + +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); +const MapLocation = require('../models/mapLocation'); +const UserProfile = require('../models/userProfile'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const mapLocationsController = require('./mapLocationsController'); + +const makeSut = () => { + const { getAllLocations, deleteLocation, putUserLocation, updateUserLocation } = + mapLocationsController(MapLocation); + + return { getAllLocations, deleteLocation, putUserLocation, updateUserLocation }; +}; + +const flushPromises = () => new Promise(setImmediate); + +const makeMockCache = (method, value) => { + const cacheObject = { + getCache: jest.fn(), + removeCache: jest.fn(), + hasCache: jest.fn(), + setCache: jest.fn(), + }; + + const mockCache = jest.spyOn(cacheObject, method).mockImplementationOnce(() => value); + + cache.mockImplementationOnce(() => cacheObject); + + return { mockCache, cacheObject }; +}; + +describe('Map Locations Controller', () => { + beforeEach(() => { + mockReq.params.locationId = 'randomId'; + mockReq.body.firstName = 'Bob'; + mockReq.body.lastName = 'Bobberson'; + mockReq.body.jobTitle = 'Software Engineer'; + mockReq.body.location = { + userProvided: 'New York', + coords: { + lat: 12, + lng: 12, + }, + country: 'USA', + city: 'New York City', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getAllLocations method', () => { + test('Returns 404 if an error occurs when finding all users.', async () => { + const { getAllLocations } = makeSut(); + + const errMsg = 'Failed to find users!'; + const findSpy = jest.spyOn(UserProfile, 'find').mockRejectedValueOnce(new Error(errMsg)); + + const res = await getAllLocations(mockReq, mockRes); + + assertResMock(404, new Error(errMsg), res, mockRes); + expect(findSpy).toHaveBeenCalledWith( + {}, + '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', + ); + }); + + test('Returns 404 if an error occurs when finding all map locations.', async () => { + const { getAllLocations } = makeSut(); + + const errMsg = 'Failed to find locations!'; + const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce([]); + const findLocationSpy = jest + .spyOn(MapLocation, 'find') + .mockRejectedValueOnce(new Error(errMsg)); + + const res = await getAllLocations(mockReq, mockRes); + + assertResMock(404, new Error(errMsg), res, mockRes); + expect(findSpy).toHaveBeenCalledWith( + {}, + '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', + ); + expect(findLocationSpy).toHaveBeenCalledWith({}); + }); + + test('Returns 200 if all is successful', async () => { + const { getAllLocations } = makeSut(); + + const findRes = [ + { + _id: 1, + firstName: 'bob', + lastName: 'marley', + isActive: true, + location: { + coords: { + lat: 12, + lng: 12, + }, + country: 'USA', + city: 'NYC', + }, + jobTitle: ['software engineer'], + totalTangibleHrs: 11, + }, + ]; + const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce(findRes); + const findLocationSpy = jest.spyOn(MapLocation, 'find').mockResolvedValueOnce([]); + + const modifiedUsers = { + location: findRes[0].location, + isActive: findRes[0].isActive, + jobTitle: findRes[0].jobTitle[0], + _id: findRes[0]._id, + firstName: findRes[0].firstName, + lastName: findRes[0].lastName, + }; + const res = await getAllLocations(mockReq, mockRes); + + assertResMock(200, { users: [modifiedUsers], mUsers: [] }, res, mockRes); + expect(findSpy).toHaveBeenCalledWith( + {}, + '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', + ); + expect(findLocationSpy).toHaveBeenCalledWith({}); + }); + }); + + describe('deleteLocation method', () => { + test('Returns 403 if user is not authorized.', async () => { + mockReq.body.requestor.role = 'Volunteer'; + const { deleteLocation } = makeSut(); + const res = await deleteLocation(mockReq, mockRes); + assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); + }); + + test('Returns 500 if an error occurs when deleting the map location.', async () => { + mockReq.body.requestor.role = 'Owner'; + + const { deleteLocation } = makeSut(); + + const err = new Error('Failed to delete!'); + const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockRejectedValueOnce(err); + + const res = await deleteLocation(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, { message: err }, res, mockRes); + expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); + }); + + test('Returns 200 if all is successful', async () => { + mockReq.body.requestor.role = 'Owner'; + const { deleteLocation } = makeSut(); + + const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockResolvedValueOnce(true); + + const res = await deleteLocation(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, { message: 'The location was successfully removed!' }, res, mockRes); + expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); + }); + }); + + describe('putUserLocation method', () => { + test('Returns 403 if user is not authorized.', async () => { + mockReq.body.requestor.role = 'Volunteer'; + const { putUserLocation } = makeSut(); + + const res = await putUserLocation(mockReq, mockRes); + assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); + }); + + test('Returns 500 if an error occurs when saving the map location.', async () => { + const { putUserLocation } = makeSut(); + + mockReq.body.requestor.role = 'Owner'; + + const err = new Error('Saving failed!'); + + jest.spyOn(MapLocation.prototype, 'save').mockImplementationOnce(() => Promise.reject(err)); + + const res = await putUserLocation(mockReq, mockRes); + + assertResMock(500, { message: err.message }, res, mockRes); + }); + + test('Returns 200 if all is successful.', async () => { + const { putUserLocation } = makeSut(); + + mockReq.body.requestor.role = 'Owner'; + + const savedLocationData = { + _id: 1, + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + }; + + jest + .spyOn(MapLocation.prototype, 'save') + .mockImplementationOnce(() => Promise.resolve(savedLocationData)); + + const res = await putUserLocation(mockReq, mockRes); + + assertResMock(200, savedLocationData, res, mockRes); + }); + }); + + describe('updateUserLocation method', () => { + test('Returns 403 if user is not authorized.', async () => { + const { updateUserLocation } = makeSut(); + + mockReq.body.requestor.role = 'Volunteer'; + + const res = await updateUserLocation(mockReq, mockRes); + + assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); + }); + + // Returns 500 if an error occurs when updating the user location. + test('Returns 500 if an error occurs when updating the user location', async () => { + const { updateUserLocation } = makeSut(); + mockReq.body.requestor.role = 'Owner'; + mockReq.body.type = 'user'; + mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; + const updateData = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + }; + + const errMsg = 'Failed to update user profile!'; + const findAndUpdateSpy = jest + .spyOn(UserProfile, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); + + const res = await updateUserLocation(mockReq, mockRes); + + assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); + expect(findAndUpdateSpy).toHaveBeenCalledWith( + { _id: mockReq.body._id }, + { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, + { new: true }, + ); + }); + + test('returns 500 if an error occurs when updating map location', async () => { + const { updateUserLocation } = makeSut(); + mockReq.body.requestor.role = 'Owner'; + mockReq.body.type = 'non-user'; + mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; + const updateData = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + }; + + const errMsg = 'failed to update map locations!'; + const findAndUpdateSpy = jest + .spyOn(MapLocation, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); + + const res = await updateUserLocation(mockReq, mockRes); + assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); + expect(findAndUpdateSpy).toHaveBeenCalledWith( + { _id: mockReq.body._id }, + { $set: updateData }, + { new: true }, + ); + }); + + test('Returns 200 if all is successful when userType is user and clears and resets cache.', async () => { + mockReq.body.requestor.role = 'Owner'; + mockReq.body.type = 'user'; + mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; + + const { mockCache: removeAllUsersMock, cacheObject } = makeMockCache('removeCache', true); + const removeUserCacheSpy = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => true); + + const setCacheSpy = jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => true); + + const { updateUserLocation } = makeSut(); + + const updateData = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + }; + + const queryResponse = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + _id: mockReq.body._id, + }; + + const findOneAndUpdateSpy = jest + .spyOn(UserProfile, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(queryResponse)); + + const res = await updateUserLocation(mockReq, mockRes); + + assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); + expect(findOneAndUpdateSpy).toHaveBeenCalledWith( + { _id: mockReq.body._id }, + { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, + { new: true }, + ); + + expect(removeAllUsersMock).toHaveBeenCalledWith('allusers'); + expect(removeUserCacheSpy).toHaveBeenCalledWith(`user-${mockReq.body._id}`); + expect(setCacheSpy).toHaveBeenCalledWith( + `user-${mockReq.body._id}`, + JSON.stringify(queryResponse), + ); + }); + + test('Returns 200 if all is succesful when userType is not user', async () => { + mockReq.body.requestor.role = 'Owner'; + mockReq.body.type = 'not-user'; + mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; + + const { updateUserLocation } = makeSut(); + + const updateData = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + }; + + const queryResponse = { + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + jobTitle: mockReq.body.jobTitle, + location: mockReq.body.location, + _id: mockReq.body._id, + }; + + const findOneAndUpdateSpy = jest + .spyOn(MapLocation, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(queryResponse)); + + const res = await updateUserLocation(mockReq, mockRes); + + assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); + expect(findOneAndUpdateSpy).toHaveBeenCalledWith( + { _id: mockReq.body._id }, + { $set: updateData }, + { new: true }, + ); + }); + }); +}); diff --git a/src/controllers/mouseoverTextController.js b/src/controllers/mouseoverTextController.js index 636f8763a..74fae9847 100644 --- a/src/controllers/mouseoverTextController.js +++ b/src/controllers/mouseoverTextController.js @@ -1,49 +1,43 @@ -const mouseoverTextController = function (MouseoverText) { - const createMouseoverText = function (req, res) { - const newMouseoverText = new MouseoverText(); - newMouseoverText.mouseoverText = req.body.newMouseoverText; - newMouseoverText - .save() - .then(() => - res.status(201).json({ - _serverMessage: 'MouseoverText succesfuly created!', - mouseoverText: newMouseoverText, - }), - ) - .catch((err) => res.status(500).send({ err })); - }; +const mouseoverTextController = (function (MouseoverText) { + const createMouseoverText = function (req, res) { + const newMouseoverText = new MouseoverText(); + newMouseoverText.mouseoverText = req.body.newMouseoverText; + newMouseoverText.save().then(() => res.status(201).json({ + _serverMessage: 'MouseoverText succesfuly created!', + mouseoverText: newMouseoverText, + })).catch(err => res.status(500).send({ err })); + }; - const getMouseoverText = function (req, res) { - MouseoverText.find() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); - }; + const getMouseoverText = function (req, res) { + MouseoverText.find() + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); + }; - const updateMouseoverText = function (req, res) { - // if (req.body.requestor.role !== 'Owner') { - // res.status(403).send('You are not authorized to update mouseoverText!'); - // } - const { id } = req.params; + const updateMouseoverText = function (req, res) { + // if (req.body.requestor.role !== 'Owner') { + // res.status(403).send('You are not authorized to update mouseoverText!'); + // } + const { id } = req.params; - return MouseoverText.findById(id, (error, mouseoverText) => { - if (error || mouseoverText === null) { - res.status(500).send('MouseoverText not found with the given ID'); - return; - } + return MouseoverText.findById(id, (error, mouseoverText) => { + if (error || mouseoverText === null) { + res.status(500).send('MouseoverText not found with the given ID'); + return; + } - mouseoverText.mouseoverText = req.body.newMouseoverText; - mouseoverText - .save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(400).send(errors)); - }); - }; + mouseoverText.mouseoverText = req.body.newMouseoverText; + mouseoverText.save() + .then(results => res.status(201).send(results)) + .catch(errors => res.status(400).send(errors)); + }); + }; - return { - createMouseoverText, - getMouseoverText, - updateMouseoverText, - }; -}; + return { + createMouseoverText, + getMouseoverText, + updateMouseoverText, + }; +}); module.exports = mouseoverTextController; diff --git a/src/controllers/mouseoverTextController.spec.js b/src/controllers/mouseoverTextController.spec.js deleted file mode 100644 index b4a5bd48b..000000000 --- a/src/controllers/mouseoverTextController.spec.js +++ /dev/null @@ -1,162 +0,0 @@ -const mouseoverTextController = require('./mouseoverTextController'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const MouseoverText = require('../models/mouseoverText'); - -const makeSut = () => { - const { createMouseoverText, getMouseoverText, updateMouseoverText } = - mouseoverTextController(MouseoverText); - return { createMouseoverText, getMouseoverText, updateMouseoverText }; -}; - -const flushPromises = () => new Promise(setImmediate); -describe('mouseoverText Controller', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockReq.params.id = '6237f9af9820a0134ca79c5g'; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('createMouseoverText method', () => { - test('Ensure createMouseoverText returns 500 if any error when saving new mouseoverText', async () => { - const { createMouseoverText } = makeSut(); - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - newMouseoverText: 'some mouseoverText', - }, - }; - jest - .spyOn(MouseoverText.prototype, 'save') - .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); - - const response = createMouseoverText(newMockReq, mockRes); - await flushPromises(); - - assertResMock(500, { err: new Error('Error when saving') }, response, mockRes); - }); - test('Ensure createMouseoverText returns 201 if create new mouseoverText successfully', async () => { - const { createMouseoverText } = makeSut(); - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - newMouseoverText: 'new mouseoverText', - }, - }; - mockRes.json = jest.fn(); - jest.spyOn(MouseoverText.prototype, 'save').mockResolvedValue({ - mouseoverText: 'new mouseoverText', - }); - createMouseoverText(newMockReq, mockRes); - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(201); - expect(mockRes.json).toHaveBeenCalledWith( - expect.objectContaining({ - mouseoverText: expect.objectContaining({ - mouseoverText: 'new mouseoverText', - }), - }), - ); - }); - }); - describe('getMouseoverText method', () => { - test('Ensure getMouseoverText returns 404 if any error when finding the mouseoverText', async () => { - const { getMouseoverText } = makeSut(); - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - mouseoverText: 'some mouseoverText', - }, - }; - jest - .spyOn(MouseoverText, 'find') - .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); - - const response = getMouseoverText(newMockReq, mockRes); - await flushPromises(); - - assertResMock(404, new Error('Error when finding'), response, mockRes); - }); - test('Ensure getMouseoverText returns 200 if get the mouseoverText successfully', async () => { - const { getMouseoverText } = makeSut(); - const data = { - mouseoverText: 'some get mouseoverText', - }; - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - mouseoverText: 'get mouseoverText', - }, - }; - jest.spyOn(MouseoverText, 'find').mockImplementationOnce(() => Promise.resolve(data)); - const response = getMouseoverText(newMockReq, mockRes); - await flushPromises(); - - assertResMock(200, data, response, mockRes); - }); - }); - describe('updateMouseoverText method', () => { - test('Ensure updateMouseoverText returns 500 if any error when finding the mouseoverText by Id', async () => { - const { updateMouseoverText } = makeSut(); - const findByIdSpy = jest - .spyOn(MouseoverText, 'findById') - .mockImplementationOnce((_, cb) => cb(true, null)); - const response = updateMouseoverText(mockReq, mockRes); - await flushPromises(); - - assertResMock(500, 'MouseoverText not found with the given ID', response, mockRes); - expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); - }); - test('Ensure updateMouseoverText returns 400 if any error when saving the mouseoverText', async () => { - const { updateMouseoverText } = makeSut(); - const data = { - mouseoverText: 'old mouseoverText', - save: () => {}, - }; - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - newMouseoverText: 'some new mouseoverText', - }, - }; - const findByIdSpy = jest - .spyOn(MouseoverText, 'findById') - .mockImplementationOnce((_, cb) => cb(false, data)); - jest.spyOn(data, 'save').mockRejectedValueOnce(new Error('Error when saving')); - const response = updateMouseoverText(newMockReq, mockRes); - await flushPromises(); - expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); - assertResMock(400, new Error('Error when saving'), response, mockRes); - }); - test('Ensure updateMouseoverText returns 201 if updating mouseoverText successfully', async () => { - const { updateMouseoverText } = makeSut(); - const data = { - mouseoverText: 'some get mouseoverText', - save: () => {}, - }; - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - newMouseoverText: 'some new mouseoverText', - }, - }; - const findByIdSpy = jest - .spyOn(MouseoverText, 'findById') - .mockImplementationOnce((_, cb) => cb(false, data)); - jest.spyOn(data, 'save').mockResolvedValueOnce(data); - const response = updateMouseoverText(newMockReq, mockRes); - await flushPromises(); - expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); - assertResMock(201, data, response, mockRes); - }); - }); -}); diff --git a/src/controllers/notificationController.js b/src/controllers/notificationController.js index e15f0d989..98911c3f9 100644 --- a/src/controllers/notificationController.js +++ b/src/controllers/notificationController.js @@ -3,8 +3,8 @@ const LOGGER = require('../startup/logger'); /** * API endpoint for notifications service. - * @param {} Notification - * @returns + * @param {} Notification + * @returns */ const notificationController = function () { @@ -18,15 +18,13 @@ const notificationController = function () { const getUserNotifications = async function (req, res) { const { userId } = req.params; const { requestor } = req.body; - if (!userId) { - res.status(400).send({ error: 'User ID is required' }); + if (requestor.requestorId !== userId && (requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { + res.status(403).send({ error: 'Unauthorized request' }); return; } - if ( - requestor.requestorId !== userId && - (requestor.role !== 'Administrator' || requestor.role !== 'Owner') - ) { - res.status(403).send({ error: 'Unauthorized request' }); + + if (!userId) { + res.status(400).send({ error: 'User ID is required' }); return; } @@ -39,7 +37,7 @@ const notificationController = function () { } }; - /** + /** * This function allows the user to get unread notifications for themselves or * allows the admin/owner user to get unread notifications for a specific user. * @param {Object} req - The request with userID as request param. @@ -49,15 +47,13 @@ const notificationController = function () { const getUnreadUserNotifications = async function (req, res) { const { userId } = req.params; const { requestor } = req.body; - if (!userId) { - res.status(400).send({ error: 'User ID is required' }); + if (requestor.requestorId !== userId && (requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { + res.status(403).send({ error: 'Unauthorized request' }); return; } - if ( - requestor.requestorId !== userId && - (requestor.role !== 'Administrator' || requestor.role !== 'Owner') - ) { - res.status(403).send({ error: 'Unauthorized request' }); + + if (!userId) { + res.status(400).send({ error: 'User ID is required' }); return; } @@ -72,13 +68,13 @@ const notificationController = function () { /** * This function allows the admin/owner user to get all notifications that they have sent. - * @param {*} req - * @param {*} res - * @returns + * @param {*} req + * @param {*} res + * @returns */ const getSentNotifications = async function (req, res) { const { requestor } = req.body; - if (requestor.role !== 'Administrator' && requestor.role !== 'Owner') { + if ((requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -92,17 +88,18 @@ const notificationController = function () { } }; + /** * This function allows the Administrator/Owner user to create a notification to specific user. * @param {*} req request with a JSON payload containing the message and recipient list. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const createUserNotification = async function (req, res) { const { message, recipient } = req.body; const sender = req.requestor.requestorId; - if (req.body.requestor.role !== 'Administrator' && req.body.requestor.role !== 'Owner') { + if (req.body.requestor.role !== 'Administrator' || req.body.requestor.role !== 'Owner') { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -124,13 +121,13 @@ const notificationController = function () { /** * This function allows the Administrator/Owner user to delete a notification. * @param {*} req request with the notification ID as a parameter. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const deleteUserNotification = async function (req, res) { const { requestor } = req.body; - if (requestor.role !== 'Administrator' && requestor.role !== 'Owner') { + if (requestor.role !== 'Administrator' || requestor.role !== 'Owner') { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -147,8 +144,8 @@ const notificationController = function () { /** * This function allows the user to mark a notification as read. * @param {*} req request with the notification ID as a parameter. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const markNotificationAsRead = async function (req, res) { const recipientId = req.body.requestor.requestorId; @@ -159,10 +156,7 @@ const notificationController = function () { } try { - const result = await notificationService.markNotificationAsRead( - req.params.notificationId, - recipientId, - ); + const result = await notificationService.markNotificationAsRead(req.params.notificationId, recipientId); res.status(200).send(result); } catch (err) { LOGGER.logException(err); diff --git a/src/controllers/notificationController.spec.js b/src/controllers/notificationController.spec.js deleted file mode 100644 index 2a7662122..000000000 --- a/src/controllers/notificationController.spec.js +++ /dev/null @@ -1,313 +0,0 @@ -const notificationController = require('./notificationController'); -const Notification = require('../models/notification'); -const notificationService = require('../services/notificationService'); -const { mockReq, mockRes, assertResMock } = require('../test'); - -const makeSut = () => { - const { - getUserNotifications, - getUnreadUserNotifications, - getSentNotifications, - createUserNotification, - deleteUserNotification, - markNotificationAsRead, - } = notificationController(Notification); - - return { - getUserNotifications, - getUnreadUserNotifications, - getSentNotifications, - createUserNotification, - deleteUserNotification, - markNotificationAsRead, - }; -}; - -describe('Notification controller Unit Tests', () => { - beforeEach(() => { - mockReq.params.userId = '65cf6c3706d8ac105827bb2e'; - mockReq.body.requestor.role = 'Administrator'; - mockReq.body.requestor = { - requestorId: '65cf6c3706d8ac105827bb2e', - role: 'Administrator', - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getUserNotifications', () => { - test('Ensures getUserNotifications returns error 400 if userId is not provided', async () => { - const { getUserNotifications } = makeSut(); - const errorMsg = { error: 'User ID is required' }; - mockReq.params.userId = ''; - const response = await getUserNotifications(mockReq, mockRes); - assertResMock(400, errorMsg, response, mockRes); - }); - test('Ensures getUserNotifications returns error 403 if userId does not match requestorId', async () => { - const { getUserNotifications } = makeSut(); - const errorMsg = { error: 'Unauthorized request' }; - mockReq.body.requestor.requestorId = 'differentUserId'; - const response = await getUserNotifications(mockReq, mockRes); - assertResMock(403, errorMsg, response, mockRes); - }); - test('Ensures getUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { - const { getUserNotifications } = makeSut(); - const mockNotifications = [ - { id: '123', message: 'Notification Test 1' }, - { id: '123', message: 'Notification Test 2' }, - { id: '123', message: 'Notification Test 3' }, - ]; - const mockService = jest.fn().mockResolvedValue(mockNotifications); - notificationService.getNotifications = mockService; - const response = await getUserNotifications(mockReq, mockRes); - assertResMock(200, mockNotifications, response, mockRes); - }); - test('Ensures getUserNotifications returns error 500 if there is an internal error while fetching notifications.', async () => { - const { getUserNotifications } = makeSut(); - const errorMsg = { error: 'Internal Error' }; - const mockService = jest.fn().mockRejectedValue(errorMsg); - notificationService.getNotifications = mockService; - const response = await getUserNotifications(mockReq, mockRes); - assertResMock(500, errorMsg, response, mockRes); - }); - }); - - describe('getUnreadUserNotifications', () => { - test('Ensures getUnreadUserNotifications returns error 400 if userId is not provided', async () => { - const { getUnreadUserNotifications } = makeSut(); - const errorMsg = { error: 'User ID is required' }; - mockReq.params.userId = ''; - const response = await getUnreadUserNotifications(mockReq, mockRes); - assertResMock(400, errorMsg, response, mockRes); - }); - test('Ensures getUnreadUserNotifications returns error 403 if userId does not match requestorId', async () => { - const { getUnreadUserNotifications } = makeSut(); - const errorMsg = { error: 'Unauthorized request' }; - mockReq.body.requestor.requestorId = 'differentUserId' - const response = await getUnreadUserNotifications(mockReq, mockRes); - assertResMock(403, errorMsg, response, mockRes); - }); - test('Ensures getUnreadUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { - const { getUnreadUserNotifications } = makeSut(); - const mockNotifications = [ - { id: '123', message: 'Notification Test 1' }, - { id: '123', message: 'Notification Test 2' }, - { id: '123', message: 'Notification Test 3' }, - ]; - const mockService = jest.fn().mockResolvedValue(mockNotifications); - notificationService.getUnreadUserNotifications = mockService; - const response = await getUnreadUserNotifications(mockReq, mockRes); - assertResMock(200, mockNotifications, response, mockRes); - }); - test('Ensures getUnreadUserNotifications returns error 500 if there is an internal error while fetching notifications.', async () => { - const { getUnreadUserNotifications } = makeSut(); - const errorMsg = { error: 'Internal Error' }; - const mockService = jest.fn().mockRejectedValue(errorMsg); - notificationService.getUnreadUserNotifications = mockService; - const response = await getUnreadUserNotifications(mockReq, mockRes); - assertResMock(500, errorMsg, response, mockRes); - }); - }); - describe('getSentNotifications', () => { - test('Ensures getSentNotifications returns error 403 if requestor role is neither Administrator or Owner', async () => { - const { getSentNotifications } = makeSut(); - const errorMsg = { error: 'Unauthorized request' }; - mockReq.body.requestor.role = 'randomRole' - const response = await getSentNotifications(mockReq, mockRes); - assertResMock(403, errorMsg, response, mockRes); - }); - test('Ensures getSentNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { - const { getSentNotifications } = makeSut(); - const mockNotifications = []; - const mockService = jest.fn().mockResolvedValue(mockNotifications); - notificationService.getSentNotifications = mockService; - const response = await getSentNotifications(mockReq, mockRes); - assertResMock(200, mockNotifications, response, mockRes); - }); - - test('Ensures getSentNotification returns error 500 if there is an internal error while fetching notifications.', async () => { - const { getSentNotifications } = makeSut(); - const errorMsg = { error: 'Internal Error' }; - const mockService = jest.fn().mockRejectedValue(errorMsg); - notificationService.getSentNotifications = mockService; - const response = await getSentNotifications(mockReq, mockRes); - assertResMock(500, errorMsg, response, mockRes); - }); - }); - describe('createUserNotification', () => { - test('Ensures createUserNotification returns error 403 when requestor role is not Admin or Owner', async () => { - const { createUserNotification } = makeSut(); - const errorMsg = { error: 'Unauthorized request' }; - mockReq.body.requestor.role = 'randomRole' - mockReq.requestor = { - requestorId: '65cf6c3706d8ac105827bb2e', - }; - const response = await createUserNotification(mockReq, mockRes); - assertResMock(403, errorMsg, response, mockRes); - }); - test('Ensures createUserNotification returns error 400 if message or recipient is missing', async () => { - const { createUserNotification } = makeSut(); - const errorMsg = { error: 'Message and recipient are required' }; - mockReq.body = { - requestor: { - role: 'Administrator', - }, - message: '', - recipient: '', - }; - const response = await createUserNotification(mockReq, mockRes); - assertResMock(400, errorMsg, response, mockRes); - }); - test('Ensures createUserNotification returns 200 and notification data when notification is created successfully', async () => { - const { createUserNotification } = makeSut(); - const mockNotification = { - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - sender: '5a7e21f00317bc1538def4b7', - }; - mockReq.body = { - requestor: { - role: 'Administrator', - }, - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - }; - mockReq.requestor = { - requestorId: '5a7e21f00317bc1538def4b7', - }; - - const mockService = jest.fn().mockResolvedValue(mockNotification); - notificationService.createNotification = mockService; - - await createUserNotification(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith(mockNotification); - expect(mockService).toHaveBeenCalledWith( - mockReq.requestor.requestorId, - mockReq.body.recipient, - mockReq.body.message, - ); - }); - test('Ensures createUserNotification returns error 500 if there is an internal error while creating a notification.', async () => { - const { createUserNotification } = makeSut(); - mockReq.body.requestor = { - requestorId: '65cf6c3706d8ac105827bb2e', - role: 'Administrator', - }; - notificationService.createNotification = jest - .fn() - .mockRejectedValue({ error: 'Internal Error' }); - await createUserNotification(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); - }); - }); - describe('deleteUserNotification', () => { - test('Ensures deleteUserNotification returns error 403 when requestor role is not Admin or Owner', async () => { - const { deleteUserNotification } = makeSut(); - mockReq.body.requestor.role = 'randomRole'; - await deleteUserNotification(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(403); - expect(mockRes.send).toHaveBeenCalledWith({ error: 'Unauthorized request' }); - }); - test('Ensures deleteUserNotification returns 200 and deletes notification', async () => { - const { deleteUserNotification } = makeSut(); - const mockNotification = { - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - sender: '5a7e21f00317bc1538def4b7', - }; - mockReq.body = { - requestor: { - role: 'Administrator', - }, - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - }; - mockReq.requestor = { - requestorId: '5a7e21f00317bc1538def4b7', - }; - - const mockService = jest.fn().mockResolvedValue(mockNotification); - notificationService.deleteNotification = mockService; - - await deleteUserNotification(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith(mockNotification); - expect(mockService).toHaveBeenCalledWith(mockReq.params.notificationId); - }); - test('Ensures deleteUserNotification returns error 500 if there is an internal error while deleting a notification.', async () => { - const { deleteUserNotification } = makeSut(); - mockReq.body.requestor = { - requestorId: '65cf6c3706d8ac105827bb2e', - role: 'Administrator', - }; - notificationService.deleteNotification = jest - .fn() - .mockRejectedValue({ error: 'Internal Error' }); - await deleteUserNotification(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); - }); - }); - describe('markNotificationsAsRead', () => { - test('Ensures markNotificationAsRead returns 400 if recipientId is missing', () => { - const { markNotificationAsRead } = makeSut(); - mockReq.body.requestor.requestorId = ''; - markNotificationAsRead(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(400); - expect(mockRes.send).toHaveBeenCalledWith({ error: 'Recipient ID is required' }); - }); - test('Ensures markNotificationAsRead returns 200 and marks notification as read', async () => { - const { markNotificationAsRead } = makeSut(); - const mockNotification = { - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - sender: '5a7e21f00317bc1538def4b7', - }; - mockReq.body = { - requestor: { - role: 'Administrator', - }, - message: 'Notification Test', - recipient: '65cf6c3706d8ac105827bb2e', - }; - mockReq.body.requestor = { - requestorId: '5a7e21f00317bc1538def4b7', - }; - mockReq.params = { - notificationId: '12345', - }; - - const mockService = jest.fn().mockResolvedValue(mockNotification); - notificationService.markNotificationAsRead = mockService; - - await markNotificationAsRead(mockReq, mockRes); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith(mockNotification); - expect(mockService).toHaveBeenCalledWith( - mockReq.params.notificationId, - mockReq.body.requestor.requestorId, - ); - }); - test('Ensures markNotificationAsRead returns 500 if there is an internal error while marking notification as read.', async () => { - const { markNotificationAsRead } = makeSut(); - mockReq.body.requestor = { - requestorId: '65cf6c3706d8ac105827bb2e', - role: 'Administrator', - }; - notificationService.markNotificationAsRead = jest - .fn() - .mockRejectedValue({ error: 'Internal Error' }); - await markNotificationAsRead(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); - }); - }); -}); diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js deleted file mode 100644 index c56044136..000000000 --- a/src/controllers/ownerMessageController.spec.js +++ /dev/null @@ -1,139 +0,0 @@ -const OwnerMessage = require('../models/ownerMessage'); -const ownerMessageController = require('./ownerMessageController'); -const { mockReq, mockRes, assertResMock } = require('../test'); - -const makeSut = () => { - const { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage } = - ownerMessageController(OwnerMessage); - return { - getOwnerMessage, - updateOwnerMessage, - deleteOwnerMessage, - }; -}; -const flushPromises = () => new Promise(setImmediate); - -describe('ownerMessageController Unit Tests', () => { - let mockFind; - let mockSave; - afterEach(() => { - jest.clearAllMocks(); - }); - - beforeEach(() => { - mockFind = jest.spyOn(OwnerMessage, 'find'); - mockSave = jest.fn(); - }); - describe('getOwnerMessage', () => { - test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { - const { getOwnerMessage } = makeSut(); - const errorMsg = 'Error occurred when finding owner message'; - mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); - const response = await getOwnerMessage(mockReq, mockRes); - await flushPromises(); - assertResMock(404, errorMsg, response, mockRes); - }); - test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { - mockFind.mockResolvedValue([]); - const ownerMessageInstance = new OwnerMessage(); - ownerMessageInstance.set = jest.fn(); - const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); - - jest.spyOn(OwnerMessage.prototype, 'save').mockImplementation(mockSaveFn); - await makeSut().getOwnerMessage(mockReq, mockRes); - await flushPromises(); - - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith( - expect.objectContaining({ - ownerMessage: expect.objectContaining({ - _id: expect.anything(), - message: '', - standardMessage: '', - }), - }), - ); - expect(mockSaveFn).toHaveBeenCalled(); - }); - - test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { - const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; - mockFind.mockResolvedValue([existingMessage]); - await makeSut().getOwnerMessage(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith({ ownerMessage: existingMessage }); - }); - }); - describe('updateOwnerMessage', () => { - test('Ensures updateOwnerMessage returns status 403 if requestor is not an owner', async () => { - const { updateOwnerMessage } = makeSut(); - const req = { body: { requestor: { role: 'User' } } }; - const response = await updateOwnerMessage(req, mockRes); - await flushPromises(); - assertResMock(403, 'You are not authorized to create messages!', response, mockRes); - }); - test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { - const existingMessage = { message: '', standardMessage: '', save: mockSave }; - mockFind.mockResolvedValue([existingMessage]); - const mockReqDup = { - ...mockReq, - body: { - ...mockReq.body, - isStandard: false, - newMessage: 'New custom message', - requestor: { role: 'Owner' }, - }, - }; - await makeSut().updateOwnerMessage(mockReqDup, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(201); - expect(mockRes.send).toHaveBeenCalledWith({ - _serverMessage: 'Update successfully!', - ownerMessage: { standardMessage: '', message: 'New custom message' }, - }); - expect(mockSave).toHaveBeenCalled(); - }); - test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { - const errorMsg = 'Error occurred during update'; - mockFind.mockRejectedValue(errorMsg); - const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; - await makeSut().updateOwnerMessage(mockReqDup, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith(errorMsg); - }); - }); - describe('deleteOwnerMessage', () => { - test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { - const { deleteOwnerMessage } = makeSut(); - mockReq.body.requestor.role = 'notOwner'; - const response = await deleteOwnerMessage(mockReq, mockRes); - await flushPromises(); - assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); - }); - test('Ensures deleteOwnerMessage returns status 200 and deletes the owner message correctly', async () => { - const existingMessage = { - message: 'Existing message', - standardMessage: 'Standard message', - save: mockSave, - }; - const { deleteOwnerMessage } = makeSut(); - mockFind.mockResolvedValue([existingMessage]); - mockReq.body.requestor.role = ''; - await deleteOwnerMessage(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(200); - expect(mockRes.send).toHaveBeenCalledWith({ - _serverMessage: 'Delete successfully!', - ownerMessage: existingMessage, - }); - expect(mockSave).toHaveBeenCalled(); - }); - test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { - const { deleteOwnerMessage } = makeSut(); - const errorMsg = 'Error occurred during delete'; - mockFind.mockRejectedValue(errorMsg); - mockReq.body.requestor.role = 'Owner'; - await deleteOwnerMessage(mockReq, mockRes); - expect(mockRes.status).toHaveBeenCalledWith(500); - expect(mockRes.send).toHaveBeenCalledWith(errorMsg); - }); - }); -}); diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index fcf24ce1a..f6086d02f 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -7,6 +7,7 @@ const config = require('../config'); const cache = require('../utilities/nodeCache')(); const LOGGER = require('../startup/logger'); + const TOKEN_HAS_SETUP_MESSAGE = 'SETUP_ALREADY_COMPLETED'; const TOKEN_CANCEL_MESSAGE = 'CANCELLED'; const TOKEN_INVALID_MESSAGE = 'INVALID'; @@ -120,10 +121,24 @@ const profileInitialSetupController = function ( ProfileInitialSetupToken, userProfile, Project, - MapLocation, + MapLocation ) { const { JWT_SECRET } = config; + const setMapLocation = async (locationData) => { + const location = new MapLocation(locationData); + + try { + const response = await location.save(); + return response; + } catch (err) { + return { + type: "Error", + message: err.message || "An error occurred while saving the location", + }; + } + }; + /** * Function to handle token generation and email process: - Generates a new token and saves it to the database. @@ -141,22 +156,16 @@ const profileInitialSetupController = function ( const expiration = moment().add(3, 'week'); // Wrap multiple db operations in a transaction const session = await startSession(); - session.startTransaction(); - + try { - const existingEmail = await userProfile - .findOne({ - email, - }) - .session(session); - + const existingEmail = await userProfile.findOne({ + email, + }); if (existingEmail) { - await session.abortTransaction(); - session.endSession(); return res.status(400).send('email already in use'); } - - await ProfileInitialSetupToken.findOneAndDelete({ email }).session(session); + session.startTransaction(); + await ProfileInitialSetupToken.findOneAndDelete({ email }); const newToken = new ProfileInitialSetupToken({ token, @@ -168,7 +177,7 @@ const profileInitialSetupController = function ( createdDate: Date.now(), }); - const savedToken = await newToken.save({ session }); + const savedToken = await newToken.save(); const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); @@ -250,112 +259,8 @@ const profileInitialSetupController = function ( }); if (existingEmail) { - return res.status(400).send('email already in use'); - } - if (foundToken) { - const expirationMoment = moment(foundToken.expiration); - - if (expirationMoment.isAfter(currentMoment)) { - const defaultProject = await Project.findOne({ - projectName: 'Orientation and Initial Setup', - }); - - const newUser = new userProfile(); - newUser.password = req.body.password; - newUser.role = 'Volunteer'; - newUser.firstName = req.body.firstName; - newUser.lastName = req.body.lastName; - newUser.jobTitle = req.body.jobTitle; - newUser.phoneNumber = req.body.phoneNumber; - newUser.bio = ''; - newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; - newUser.weeklycommittedHoursHistory = [ - { - hours: newUser.weeklycommittedHours, - dateChanged: Date.now(), - }, - ]; - newUser.personalLinks = []; - newUser.adminLinks = []; - newUser.teams = Array.from(new Set([])); - newUser.projects = Array.from(new Set([defaultProject])); - newUser.createdDate = Date.now(); - newUser.email = req.body.email; - newUser.weeklySummaries = [{ summary: '' }]; - newUser.weeklySummariesCount = 0; - newUser.weeklySummaryOption = 'Required'; - newUser.mediaUrl = ''; - newUser.collaborationPreference = req.body.collaborationPreference; - newUser.timeZone = req.body.timeZone || 'America/Los_Angeles'; - newUser.location = req.body.location; - newUser.profilePic = req.body.profilePicture; - newUser.permissions = { - frontPermissions: [], - backPermissions: [], - }; - newUser.bioPosted = 'default'; - newUser.privacySettings.email = req.body.privacySettings.email; - newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; - newUser.teamCode = ''; - newUser.isFirstTimelog = true; - newUser.homeCountry = req.body.homeCountry || req.body.location; - - const savedUser = await newUser.save(); - - emailSender( - process.env.MANAGER_EMAIL || 'jae@onecommunityglobal.org', // "jae@onecommunityglobal.org" - `NEW USER REGISTERED: ${savedUser.firstName} ${savedUser.lastName}`, - informManagerMessage(savedUser), - null, - null, - ); - await ProfileInitialSetupToken.findByIdAndDelete(foundToken._id); - - const jwtPayload = { - userid: savedUser._id, - role: savedUser.role, - permissions: savedUser.permissions, - expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), - }; - - const token = jwt.sign(jwtPayload, JWT_SECRET); - - const locationData = { - title: '', - firstName: req.body.firstName, - lastName: req.body.lastName, - jobTitle: req.body.jobTitle, - location: req.body.homeCountry, - isActive: true, - }; - - res.send({ token }).status(200); - - const mapEntryResult = await setMapLocation(locationData); - if (mapEntryResult.type === 'Error') { - console.log(mapEntryResult.message); - } - - const NewUserCache = { - permissions: savedUser.permissions, - isActive: true, - weeklycommittedHours: savedUser.weeklycommittedHours, - createdDate: savedUser.createdDate.toISOString(), - _id: savedUser._id, - role: savedUser.role, - firstName: savedUser.firstName, - lastName: savedUser.lastName, - email: savedUser.email, - }; - - const allUserCache = JSON.parse(cache.getCache('allusers')); - allUserCache.push(NewUserCache); - cache.setCache('allusers', JSON.stringify(allUserCache)); - } else { - return res.status(400).send('Token is expired'); - } - } else { - return res.status(400).send('Invalid token'); + res.status(400).send('email already in use'); + return; } const expirationMoment = moment(foundToken.expiration); @@ -411,7 +316,6 @@ const profileInitialSetupController = function ( newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; newUser.teamCode = ''; newUser.isFirstTimelog = true; - newUser.homeCountry = req.body.homeCountry || req.body.location; const savedUser = await newUser.save(); @@ -432,6 +336,15 @@ const profileInitialSetupController = function ( const jwtToken = jwt.sign(jwtPayload, JWT_SECRET); + const locationData = { + title: '', + firstName: req.body.firstName, + lastName: req.body.lastName, + jobTitle: req.body.jobTitle, + location: req.body.homeCountry, + isActive: true, + }; + res.status(200).send({ token: jwtToken }); await ProfileInitialSetupToken.findOneAndUpdate( { _id: foundToken._id }, @@ -439,6 +352,11 @@ const profileInitialSetupController = function ( { new: true }, ); + const mapEntryResult = await setMapLocation(locationData); + if (mapEntryResult.type === 'Error') { + console.log(mapEntryResult.message); + } + const NewUserCache = { permissions: savedUser.permissions, isActive: true, @@ -472,9 +390,10 @@ const profileInitialSetupController = function ( const foundToken = await ProfileInitialSetupToken.findOne({ token }); if (foundToken) { - return res.status(200).send({ userAPIKey: premiumKey }); + res.status(200).send({ userAPIKey: premiumKey }); + } else { + res.status(403).send("Unauthorized Request"); } - return res.status(403).send('Unauthorized Request'); }; function calculateTotalHours(hoursByCategory) { @@ -488,11 +407,16 @@ const profileInitialSetupController = function ( const getTotalCountryCount = async (req, res) => { try { const users = []; - const results = await userProfile.find({}, 'location totalTangibleHrs hoursByCategory'); + const results = await userProfile.find( + {}, + "location totalTangibleHrs hoursByCategory" + ); results.forEach((item) => { if ( - (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || + (item.location?.coords.lat && + item.location?.coords.lng && + item.totalTangibleHrs >= 10) || (item.location?.coords.lat && item.location?.coords.lng && calculateTotalHours(item.hoursByCategory) >= 10) @@ -500,21 +424,22 @@ const profileInitialSetupController = function ( users.push(item); } }); - const modifiedUsers = users.map((item) => ({ + const modifiedUsers = users.map(item => ({ location: item.location, })); const mapUsers = await MapLocation.find({}); const combined = [...modifiedUsers, ...mapUsers]; - const countries = combined.map((user) => user.location.country); + const countries = combined.map(user => user.location.country); const totalUniqueCountries = [...new Set(countries)].length; - return res.status(200).send({ CountryCount: totalUniqueCountries }); + res.status(200).send({ CountryCount: totalUniqueCountries }); } catch (error) { - LOGGER.logException(error, 'Error in getTotalCountryCount'); - return res.status(500).send(`Error: ${error}`); + res.status(500).send(`Error: ${error}`); } }; + + /** * Returns a list of setup token in not completed status * @param {*} req HTTP request include requester role information @@ -524,36 +449,29 @@ const profileInitialSetupController = function ( const getSetupInvitation = (req, res) => { const { role } = req.body.requestor; if (role === 'Administrator' || role === 'Owner') { - try { - ProfileInitialSetupToken.find({ isSetupCompleted: false }) - .sort({ createdDate: -1 }) - .exec((err, result) => { - // Handle the result - if (err) { - LOGGER.logException(err); - return res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', - ); - } - return res.status(200).send(result); - }); - } catch (error) { - LOGGER.logException(error); - return res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', - ); - } + try{ + ProfileInitialSetupToken + .find({ isSetupCompleted: false }) + .sort({ createdDate: -1 }) + .exec((err, result) => { + // Handle the result + if (err) { + LOGGER.logException(err); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + return res.status(200).send(result); + }); + } catch (error) { + LOGGER.logException(error); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } } else { return res.status(403).send('You are not authorized to get setup history.'); } }; /** - * Cancel the setup token + * Cancel the setup token * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully cancelled * @returns @@ -563,40 +481,33 @@ const profileInitialSetupController = function ( const { token } = req.body; if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken.findOneAndUpdate( - { token }, - { isCancelled: true }, - (err, result) => { - if (err) { - LOGGER.logException(err); - return res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', - ); - } - sendEmailWithAcknowledgment( - result.email, - 'One Community: Your Profile Setup Link Has Been Deactivated', - sendCancelLinkMessage(), - ); - return res.status(200).send(result); - }, - ); - } catch (error) { - LOGGER.logException(error); - return res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ProfileInitialSetupToken + .findOneAndUpdate( + { token }, + { isCancelled: true }, + (err, result) => { + if (err) { + LOGGER.logException(err); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + sendEmailWithAcknowledgment( + result.email, + 'One Community: Your Profile Setup Link Has Been Deactivated', + sendCancelLinkMessage(), ); + return res.status(200).send(result); + }, + ); + } catch (error) { + LOGGER.logException(error); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); } } else { res.status(403).send('You are not authorized to cancel setup invitation.'); } }; - /** - * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. + /** + * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully refreshed * @returns updated result of the setup invitation record. @@ -607,37 +518,30 @@ const profileInitialSetupController = function ( if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken.findOneAndUpdate( + ProfileInitialSetupToken + .findOneAndUpdate( { token }, { expiration: moment().add(3, 'week'), isCancelled: false, }, ) - .then((result) => { - const { email } = result; - const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; - sendEmailWithAcknowledgment( - email, - 'Invitation Link Refreshed: Complete Your One Community Profile Setup', - sendRefreshedLinkMessage(link), - ); - return res.status(200).send(result); - }) - .catch((err) => { - LOGGER.logException(err); - res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', - ); - }); - } catch (error) { - return res - .status(500) - .send( - 'Internal Error: Please retry. If the problem persists, please contact the administrator', + .then((result) => { + const { email } = result; + const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; + sendEmailWithAcknowledgment( + email, + 'Invitation Link Refreshed: Complete Your One Community Profile Setup', + sendRefreshedLinkMessage(link), ); + return res.status(200).send(result); + }) + .catch((err) => { + LOGGER.logException(err); + res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + }); + } catch (error) { + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); } } else { return res.status(403).send('You are not authorized to refresh setup invitation.'); @@ -677,4 +581,4 @@ const profileInitialSetupController = function ( }; }; -module.exports = profileInitialSetupController; +module.exports = profileInitialSetupController; \ No newline at end of file diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 72522ba80..b149416b7 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -1,53 +1,45 @@ - /* eslint-disable quotes */ /* eslint-disable arrow-parens */ -const mongoose = require('mongoose'); -const timeentry = require('../models/timeentry'); -const task = require('../models/task'); -const wbs = require('../models/wbs'); -const userProfile = require('../models/userProfile'); -const { hasPermission } = require('../utilities/permissions'); -const escapeRegex = require('../utilities/escapeRegex'); -const logger = require('../startup/logger'); -const cache = require('../utilities/nodeCache')(); +const mongoose = require("mongoose"); +const timeentry = require("../models/timeentry"); +const userProfile = require("../models/userProfile"); +const userProject = require("../helpers/helperModels/userProjects"); +const { hasPermission } = require("../utilities/permissions"); +const escapeRegex = require("../utilities/escapeRegex"); +const cache = require("../utilities/nodeCache")(); const projectController = function (Project) { - const getAllProjects = async function (req, res) { - try { - const projects = await Project.find( - { isArchived: { $ne: true } }, - 'projectName isActive category modifiedDatetime membersModifiedDatetime', - ).sort({ modifiedDatetime: -1 }); - res.status(200).send(projects); - } catch (error) { - logger.logException(error); - res.status(404).send('Error fetching projects. Please try again.'); - } + const getAllProjects = function (req, res) { + Project.find({}, "projectName isActive category modifiedDatetime") + .sort({ modifiedDatetime: -1 }) + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => res.status(404).send(error)); }; const deleteProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'deleteProject'))) { - res.status(403).send({ error: 'You are not authorized to delete projects.' }); + if (!(await hasPermission(req.body.requestor, "deleteProject"))) { + res + .status(403) + .send({ error: "You are not authorized to delete projects." }); return; } const { projectId } = req.params; Project.findById(projectId, (error, record) => { if (error || !record || record === null || record.length === 0) { - - res.status(400).send({ error: 'No valid records found' }); - + res.status(400).send({ error: "No valid records found" }); return; } - // find if project has any time entries associated with it + // find if project has any time entries associated with it - timeentry.find({ projectId: record._id }, '_id').then((timeentries) => { + timeentry.find({ projectId: record._id }, "_id").then((timeentries) => { if (timeentries.length > 0) { res.status(400).send({ error: - 'This project has associated time entries and cannot be deleted. Consider inactivaing it instead.', + "This project has associated time entries and cannot be deleted. Consider inactivaing it instead.", }); - } else { const removeprojectfromprofile = userProfile .updateMany({}, { $pull: { projects: record._id } }) @@ -56,11 +48,10 @@ const projectController = function (Project) { Promise.all([removeprojectfromprofile, removeproject]) .then( - res.status(200).send({ - message: 'Project successfully deleted and user profiles updated.', - }), - + message: + "Project successfully deleted and user profiles updated.", + }) ) .catch((errors) => { res.status(400).send(errors); @@ -73,154 +64,98 @@ const projectController = function (Project) { }; const postProject = async function (req, res) { - - if (!(await hasPermission(req.body.requestor, 'postProject'))) { - return res.status(401).send('You are not authorized to create new projects.'); + if (!(await hasPermission(req.body.requestor, "postProject"))) { + res + .status(403) + .send({ error: "You are not authorized to create new projects." }); + return; } - if (!req.body.projectName) { - return res.status(400).send('Project Name is mandatory fields.'); + if (!req.body.projectName || !req.body.isActive) { + res.status(400).send({ + error: "Project Name and active status are mandatory fields.", + }); + return; } - try { - const projectWithRepeatedName = await Project.find({ - projectName: { - $regex: escapeRegex(req.body.projectName), - $options: 'i', - }, - }); - if (projectWithRepeatedName.length > 0) { - res - .status(400) - .send( - `Project Name must be unique. Another project with name ${req.body.projectName} already exists. Please note that project names are case insensitive.`, - ); + Project.find({ + projectName: { $regex: escapeRegex(req.body.projectName), $options: "i" }, + }).then((result) => { + if (result.length > 0) { + res.status(400).send({ + error: `Project Name must be unique. Another project with name ${result.projectName} already exists. Please note that project names are case insensitive.`, + }); return; } const _project = new Project(); - const now = new Date(); _project.projectName = req.body.projectName; - _project.category = req.body.projectCategory; - _project.isActive = true; - _project.createdDatetime = now; - _project.modifiedDatetime = now; - const savedProject = await _project.save(); - return res.status(200).send(savedProject); - } catch (error) { - logger.logException(error); - res.status(400).send('Error creating project. Please try again.'); - } + _project.category = req.body.projectCategory || "Unspecified"; + _project.isActive = req.body.isActive; + _project.createdDatetime = Date.now(); + _project.modifiedDatetime = Date.now(); + + _project + .save() + .then((results) => res.status(201).send(results)) + .catch((error) => res.status(500).send({ error })); + }); }; const putProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "editProject"))) { - if (!(await hasPermission(req.body.requestor, 'putProject'))) { - res.status(403).send('You are not authorized to make changes in the projects.'); - return; - } - } - const { projectName, category, isActive, _id: projectId, isArchived } = req.body; - const sameNameProejct = await Project.find({ - projectName, - _id: { $ne: projectId }, - }); - if (sameNameProejct.length > 0) { - res.status(400).send('This project name is already taken'); + if (!(await hasPermission(req.body.requestor, "putProject"))) { + res + .status(403) + .send("You are not authorized to make changes in the projects."); return; } - const session = await mongoose.startSession(); - session.startTransaction(); - try { - const targetProject = await Project.findById(projectId); - if (!targetProject) { - res.status(400).send('No valid records found'); + + const { projectId } = req.params; + Project.findById(projectId, (error, record) => { + if (error || record === null) { + res.status(400).send("No valid records found"); return; } - targetProject.projectName = projectName; - targetProject.category = category; - targetProject.isActive = isActive; - targetProject.modifiedDatetime = Date.now(); - if (isArchived) { - targetProject.isArchived = isArchived; - // deactivate wbs within target project - await wbs.updateMany({ projectId }, { isActive: false }, { session }); - // deactivate tasks within affected wbs - const deactivatedwbsIds = await wbs.find({ projectId }, '_id'); - await task.updateMany( - { wbsId: { $in: deactivatedwbsIds } }, - { isActive: false }, - { session }, - ); - // remove project from userprofiles.projects array - await userProfile.updateMany( - { projects: projectId }, - { $pull: { projects: projectId } }, - { session }, - ); - // deactivate timeentry for affected tasks - await timeentry.updateMany({ projectId }, { isActive: false }, { session }); - } - await targetProject.save({ session }); - await session.commitTransaction(); - res.status(200).send(targetProject); - } catch (error) { - await session.abortTransaction(); - logger.logException(error); - res.status(400).send('Error updating project. Please try again.'); - } finally { - session.endSession(); - } + + record.projectName = req.body.projectName; + record.category = req.body.category; + record.isActive = req.body.isActive; + record.modifiedDatetime = Date.now(); + + record + .save() + .then((results) => res.status(201).send(results._id)) + .catch((errors) => res.status(400).send(errors)); + }); }; const getProjectById = function (req, res) { const { projectId } = req.params; - Project.findById(projectId, '-__v -createdDatetime -modifiedDatetime') + Project.findById(projectId, "-__v -createdDatetime -modifiedDatetime") .then((results) => res.status(200).send(results)) - .catch((err) => { - logger.logException(err); - res.status(404).send('Error fetching project. Please try again.'); - }); + .catch((error) => res.status(404).send(error)); }; - const getUserProjects = async function (req, res) { - try { - const { userId } = req.params; - const user = await userProfile.findById(userId, 'projects'); - if (!user) { - res.status(400).send('Invalid user'); - return; - } - const { projects } = user; - const projectList = await Project.find( - { _id: { $in: projects }, isActive: { $ne: false } }, - '_id projectName category', - ); - const result = projectList - .map((p) => { - p = p.toObject(); - p.projectId = p._id; - delete p._id; - return p; - }) - .sort((p1, p2) => { - if (p1.projectName.toLowerCase() < p2.projectName.toLowerCase()) return -1; - if (p1.projectName.toLowerCase() > p2.projectName.toLowerCase()) return 1; - return 0; - }); - res.status(200).send(result); - } catch (error) { - logger.logException(error); - res.status(400).send('Error fetching projects. Please try again.'); - } + const getUserProjects = function (req, res) { + const { userId } = req.params; + userProject + .findById(userId) + .then((results) => { + res.status(200).send(results.projects); + }) + .catch((error) => { + res.status(400).send(error); + }); }; const assignProjectToUsers = async function (req, res) { // verify requestor is administrator, projectId is passed in request params and is valid mongoose objectid, and request body contains an array of users - if (!(await hasPermission(req.body.requestor, 'assignProjectToUsers'))) { - res.status(403).send('You are not authorized to perform this operation'); + if (!(await hasPermission(req.body.requestor, "assignProjectToUsers"))) { + res + .status(403) + .send({ error: "You are not authorized to perform this operation" }); return; } @@ -230,22 +165,16 @@ const projectController = function (Project) { !req.body.users || req.body.users.length === 0 ) { - - res.status(400).send('Invalid request'); + res.status(400).send({ error: "Invalid request" }); return; } - const now = new Date(); + // verify project exists - Project.findByIdAndUpdate( - req.params.projectId, - { - $set: { membersModifiedDatetime: now }, - }, - { new: true }, - ) + + Project.findById(req.params.projectId) .then((project) => { if (!project || project.length === 0) { - res.status(400).send('Invalid project'); + res.status(400).send({ error: "Invalid project" }); return; } const { users } = req.body; @@ -257,7 +186,7 @@ const projectController = function (Project) { if (cache.hasCache(`user-${userId}`)) { cache.removeCache(`user-${userId}`); } - if (operation === 'Assign') { + if (operation === "Assign") { assignlist.push(userId); } else { unassignlist.push(userId); @@ -265,10 +194,16 @@ const projectController = function (Project) { }); const assignPromise = userProfile - .updateMany({ _id: { $in: assignlist } }, { $addToSet: { projects: project._id } }) + .updateMany( + { _id: { $in: assignlist } }, + { $addToSet: { projects: project._id } } + ) .exec(); const unassignPromise = userProfile - .updateMany({ _id: { $in: unassignlist } }, { $pull: { projects: project._id } }) + .updateMany( + { _id: { $in: unassignlist } }, + { $pull: { projects: project._id } } + ) .exec(); Promise.all([assignPromise, unassignPromise]) @@ -279,24 +214,21 @@ const projectController = function (Project) { res.status(500).send({ error }); }); }) - .catch((err) => { - logger.logException(err); - res.status(500).send('Error fetching project. Please try again.'); + .catch((error) => { + res.status(500).send({ error }); }); }; - const getprojectMembership = async function (req, res) { + const getprojectMembership = function (req, res) { const { projectId } = req.params; if (!mongoose.Types.ObjectId.isValid(projectId)) { - res.status(400).send('Invalid request'); + res.status(400).send({ error: "Invalid request" }); return; } - const getId = await hasPermission(req.body.requestor, 'getProjectMembers'); - userProfile .find( { projects: projectId }, - { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: getId }, + "_id firstName lastName isActive profilePic" ) .sort({ firstName: 1, lastName: 1 }) .then((results) => { diff --git a/src/controllers/reportsController.js b/src/controllers/reportsController.js index ba629897f..baca9ca13 100644 --- a/src/controllers/reportsController.js +++ b/src/controllers/reportsController.js @@ -1,80 +1,10 @@ -/* eslint-disable consistent-return */ const mongoose = require('mongoose'); -const reporthelperClosure = require('../helpers/reporthelper'); -const overviewReportHelperClosure = require('../helpers/overviewReportHelper'); +const reporthelper = require('../helpers/reporthelper')(); const { hasPermission } = require('../utilities/permissions'); const UserProfile = require('../models/userProfile'); +const userhelper = require('../helpers/userHelper')(); const reportsController = function () { - const overviewReportHelper = overviewReportHelperClosure(); - const reporthelper = reporthelperClosure(); - /** - * Aggregates all the data needed for the volunteer stats page - * # Active volunteers - * # New volunteers - * # Deactivated volunteers - * # Badges awarded - * Location data aggregation - * Weekly anniversaries - * Blue square stats - * In teams stats - */ - const getVolunteerStatsData = async (req, res) => { - const { startDate, endDate } = req.query; - if (!startDate || !endDate) { - return res.status(400).send({ msg: 'Please provide a start and end date' }); - } - const isoStartDate = new Date(startDate); - const isoEndDate = new Date(endDate); - - try { - const [ - volunteerNumberStats, - volunteerHoursStats, - totalHoursWorked, - tasksStats, - workDistributionStats, - roleDistributionStats, - usersInTeamStats, - // blueSquareStats, - anniversaryStats, - totalBadgesAwarded, - totalActiveTeams, - userLocations, - ] = await Promise.all([ - overviewReportHelper.getVolunteerNumberStats(isoStartDate, isoEndDate), - overviewReportHelper.getHoursStats(isoStartDate, isoEndDate), - overviewReportHelper.getTotalHoursWorked(isoStartDate, isoEndDate), - overviewReportHelper.getTasksStats(isoStartDate, isoEndDate), - overviewReportHelper.getWorkDistributionStats(isoStartDate, isoEndDate), - overviewReportHelper.getRoleDistributionStats(), - overviewReportHelper.getTeamMembersCount(), - // overviewReportHelper.getBlueSquareStats(startDate, endDate), - overviewReportHelper.getAnniversaries(startDate, endDate), - overviewReportHelper.getTotalBadgesAwardedCount(startDate, endDate), - overviewReportHelper.getTotalActiveTeamCount(), - overviewReportHelper.getMapLocations(), - ]); - res.status(200).send({ - volunteerNumberStats, - volunteerHoursStats, - totalHoursWorked, - tasksStats, - workDistributionStats, - roleDistributionStats, - usersInTeamStats, - // blueSquareStats, - anniversaryStats, - totalBadgesAwarded, - totalActiveTeams, - userLocations, - }); - } catch (err) { - console.log(err); - res.status(500).send({ msg: 'Error occured while fetching data. Please try again!' }); - } - }; - const getWeeklySummaries = async function (req, res) { if (!(await hasPermission(req.body.requestor, 'getWeeklySummaries'))) { res.status(403).send('You are not authorized to view all users'); @@ -87,205 +17,7 @@ const reportsController = function () { const summaries = reporthelper.formatSummaries(results); res.status(200).send(summaries); }) - .catch((error) => res.status(404).send(error)); - }; - - /** - * Gets the Volunteer Role Stats, it contains - * 1. 4+ members team count - * 2. Total badges awarded count - * 3. Number of users celebrating their anniversary - * 4. Number of members in team and not in team, with percentage - * 5. Number of active and inactive users - * - * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) - * @param {*} res - */ - const getVolunteerStats = async function (req, res) { - try { - const { startDate, endDate } = req.query; - - if (!startDate || !endDate) { - res.status(400).send('Please provide startDate and endDate'); - return; - } - - // 1. 4+ members team count - const fourPlusMembersTeamCount = await overviewReportHelper.getFourPlusMembersTeamCount(); - - // 2. Total badges awarded count - const badgeCountQuery = await overviewReportHelper.getTotalBadgesAwardedCount( - startDate, - endDate, - ); - const badgeAwardedCount = badgeCountQuery.length > 0 ? badgeCountQuery[0].badgeCollection : 0; - - // 3. Number of users celebrating their anniversary - const anniversaryCountQuery = await overviewReportHelper.getAnniversaryCount( - startDate, - endDate, - ); - const anniversaryCount = - anniversaryCountQuery.length > 0 ? anniversaryCountQuery[0].anniversaryCount : 0; - - // 4. Number of members in team and not in team, with percentage - const teamMembersCount = await overviewReportHelper.getTeamMembersCount(); - - // 5. Number of active and inactive users - const activeInactiveUsersCount = await overviewReportHelper.getActiveInactiveUsersCount(); - - const volunteerStats = { - fourPlusMembersTeamCount, - badgeAwardedCount, - anniversaryCount, - teamMembersCount, - activeInactiveUsersCount, - }; - - res.status(200).json(volunteerStats); - } catch (error) { - res.status(404).send(error); - } - }; - - /** - * Gets the Volunteer Hours Stats, it groups the users based on the number of hours they have logged - * Every ten hours is a group, so 0-9 hours, 10-19 hours, 20-29 hours, and finally 60+ hours - * It also groups users based off the percentage of their weeklycommittedHours worked for the current and previous week. - * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) - * @param {*} res - */ - const getVolunteerHoursStats = async function (req, res) { - try { - const { startDate, endDate, lastWeekStartDate, lastWeekEndDate } = req.query; - - if (!startDate || !endDate) { - res.status(400).send('Please provide startDate and endDate'); - return; - } - - const volunteerHoursStats = await overviewReportHelper.getVolunteerHoursStats( - startDate, - endDate, - lastWeekStartDate, - lastWeekEndDate, - ); - res.status(200).json(volunteerHoursStats); - } catch (error) { - console.log(error); - res.status(404).send(error); - } - }; - - /** - * Gets the Volunteer Role Stats, it contains - * 1. 4+ members team count - * 2. Total badges awarded count - * 3. Number of users celebrating their anniversary - * 4. role and count of users - * - * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) - * @param {*} res - */ - const getVolunteerRoleStats = async function (req, res) { - try { - const { startDate, endDate } = req.query; - - if (!startDate || !endDate) { - res.status(400).send('Please provide startDate and endDate'); - return; - } - - // 1. 4+ members team count - const fourPlusMembersTeamCount = await overviewReportHelper.getFourPlusMembersTeamCount(); - - // 2. Total badges awarded count - const badgeCountQuery = await overviewReportHelper.getTotalBadgesAwardedCount( - startDate, - endDate, - ); - const badgeAwardedCount = badgeCountQuery.length > 0 ? badgeCountQuery[0].badgeCollection : 0; - - // 3. Number of users celebrating their anniversary - const anniversaryCountQuery = await overviewReportHelper.getAnniversaryCount( - startDate, - endDate, - ); - const anniversaryCount = - anniversaryCountQuery.length > 0 ? anniversaryCountQuery[0].anniversaryCount : 0; - - // 4. role and count of users - const roleQuery = await overviewReportHelper.getRoleCount(); - - const roles = roleQuery.map((role) => ({ - role: role._id, - count: role.count, - })); - - const volunteerRoleStats = { - fourPlusMembersTeamCount, - badgeAwardedCount, - anniversaryCount, - roles, - }; - - res.status(200).json(volunteerRoleStats); - } catch (error) { - res.status(404).send(error); - } - }; - - /** - * Gets the Task and Project Stats, it contains - * 1. Total hours logged in tasks - * 2. Total hours logged in projects - * 3. Number of member with tasks assigned - * 4. Number of member without tasks assigned - * @param {*} req: params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) - * @param {*} res - * - */ - const getTaskAndProjectStats = async function (req, res) { - try { - const { startDate, endDate } = req.query; - - if (!startDate || !endDate) { - res.status(400).send('Please provide startDate and endDate'); - return; - } - - const taskAndProjectStats = await overviewReportHelper.getTaskAndProjectStats( - startDate, - endDate, - ); - res.status(200).json(taskAndProjectStats); - } catch (error) { - res.status(404).send(error); - } - }; - - /** - * Gets the Blue Square Stats, it filters the data based on the startDate and endDate - * @param {*} req: params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) - * @param {*} res - * @todo: Currently, infrigements do not contain a type field, so we are unable to group by type and count the number of infringements. - */ - const getBlueSquareStats = async function (req, res) { - try { - const { startDate, endDate } = req.query; - - if (!startDate || !endDate) { - res.status(400).send('Please provide startDate and endDate'); - return; - } - - const blueSquareStats = await overviewReportHelper.getBlueSquareStats(startDate, endDate); - const blueSquareCount = blueSquareStats.length > 0 ? blueSquareStats[0].infringements : 0; - - res.status(200).json({ msg: { blueSquareCount } }); - } catch (error) { - res.status(404).send(error); - } + .catch(error => res.status(404).send(error)); }; /** @@ -356,7 +88,6 @@ const reportsController = function () { } }; - // eslint-disable-next-line consistent-return const saveReportsRecepients = (req, res) => { const { userid } = req.params; const id = userid; @@ -382,7 +113,9 @@ const reportsController = function () { res.status(404).send('No valid records found'); return; } - res.status(200).send({ message: 'updated user record with getWeeklyReport true' }); + res + .status(200) + .send({ message: 'updated user record with getWeeklyReport true' }); }) .catch((err) => { console.log('error in catch block last:', err); @@ -394,16 +127,10 @@ const reportsController = function () { }; return { - getVolunteerStats, - getVolunteerHoursStats, - getTaskAndProjectStats, getWeeklySummaries, getReportRecipients, deleteReportsRecepients, saveReportsRecepients, - getVolunteerRoleStats, - getBlueSquareStats, - getVolunteerStatsData, }; }; diff --git a/src/controllers/rolePresetsController.js b/src/controllers/rolePresetsController.js index daf1c10ab..79d324bbf 100644 --- a/src/controllers/rolePresetsController.js +++ b/src/controllers/rolePresetsController.js @@ -1,9 +1,9 @@ -const helper = require('../utilities/permissions'); +const { hasPermission } = require("../utilities/permissions"); const rolePresetsController = function (Preset) { const getPresetsByRole = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } @@ -18,14 +18,14 @@ const rolePresetsController = function (Preset) { }; const createNewPreset = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } if (!req.body.roleName || !req.body.presetName || !req.body.permissions) { res.status(400).send({ - error: 'roleName, presetName, and permissions are mandatory fields.', + error: "roleName, presetName, and permissions are mandatory fields.", }); return; } @@ -34,15 +34,14 @@ const rolePresetsController = function (Preset) { preset.roleName = req.body.roleName; preset.presetName = req.body.presetName; preset.permissions = req.body.permissions; - preset - .save() - .then((result) => res.status(201).send({ newPreset: result, message: 'New preset created' })) - .catch((error) => res.status(400).send({ error })); + preset.save() + .then(result => res.status(201).send({ newPreset: result, message: 'New preset created' })) + .catch(error => res.status(400).send({ error })); }; const updatePresetById = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } @@ -52,29 +51,27 @@ const rolePresetsController = function (Preset) { record.roleName = req.body.roleName; record.presetName = req.body.presetName; record.permissions = req.body.permissions; - record - .save() - .then((results) => res.status(200).send(results)) - .catch((errors) => res.status(400).send(errors)); + record.save() + .then(results => res.status(200).send(results)) + .catch(errors => res.status(400).send(errors)); }) - .catch((error) => res.status(400).send({ error })); + .catch(error => res.status(400).send({ error })); }; const deletePresetById = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } const { presetId } = req.params; Preset.findById(presetId) .then((result) => { - result - .remove() + result.remove() .then(res.status(200).send({ message: 'Deleted preset' })) - .catch((error) => res.status(400).send({ error })); + .catch(error => res.status(400).send({ error })); }) - .catch((error) => res.status(400).send({ error })); + .catch(error => res.status(400).send({ error })); }; return { diff --git a/src/controllers/rolePresetsController.spec.js b/src/controllers/rolePresetsController.spec.js deleted file mode 100644 index d009be44e..000000000 --- a/src/controllers/rolePresetsController.spec.js +++ /dev/null @@ -1,444 +0,0 @@ -const rolePresetsController = require('./rolePresetsController'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const Preset = require('../models/rolePreset'); -const Role = require('../models/role'); -const UserProfile = require('../models/userProfile'); -const helper = require('../utilities/permissions'); - -// Mock the models -jest.mock('../models/role'); -jest.mock('../models/userProfile'); - -const makeSut = () => { - const { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById } = - rolePresetsController(Preset); - return { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById }; -}; - -const flushPromises = () => new Promise(setImmediate); - -describe('rolePresets Controller', () => { - beforeEach(() => { - jest.clearAllMocks(); - Role.findOne = jest.fn().mockReturnValue({ - exec: jest.fn().mockResolvedValue({ permissions: ['someOtherPermission'] }), - }); - UserProfile.findById = jest.fn().mockReturnValue({ - select: jest.fn().mockReturnValue({ - exec: jest.fn().mockResolvedValue({ permissions: { frontPermissions: [] } }), - }), - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('createNewPreset method', () => { - test("Ensure createNewPresets returns 403 if user doesn't have permissions for putRole", async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(false)); - - const response = await createNewPreset(mockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); - - assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); - }); - test('Ensure createNewPresetsreturns 400 if missing roleName', async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - body: { - ...mockReq.body, - presetName: 'testPreset', - premissions: ['testPremissions'], - }, - }; - const response = await createNewPreset(newMockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock( - 400, - { - error: 'roleName, presetName, and permissions are mandatory fields.', - }, - response, - mockRes, - ); - }); - test('Ensure createNewPresets returns 400 if missing presetName', async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - body: { - ...mockReq.body, - roleName: 'testRole', - premissions: ['testPremissions'], - }, - }; - const response = await createNewPreset(newMockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock( - 400, - { - error: 'roleName, presetName, and permissions are mandatory fields.', - }, - response, - mockRes, - ); - }); - test('Ensure createNewPresets returns 400 if missing permissions', async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - body: { - ...mockReq.body, - roleName: 'testRole', - presetName: 'testPreset', - }, - }; - const response = await createNewPreset(newMockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock( - 400, - { - error: 'roleName, presetName, and permissions are mandatory fields.', - }, - response, - mockRes, - ); - }); - test('Ensure createNewPresets returns 400 if any error when saving new preset', async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - roleName: 'some roleName', - presetName: 'some Preset', - permissions: ['test', 'write'], - }, - }; - jest - .spyOn(Preset.prototype, 'save') - .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); - - const response = await createNewPreset(newMockReq, mockRes); - await flushPromises(); - - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock(400, { error: new Error('Error when saving') }, response, mockRes); - }); - test('Ensure createNewPresets returns 201 if saving new preset successfully', async () => { - const { createNewPreset } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const data = { - roleName: 'testRoleName', - presetName: 'testPresetName', - premissions: ['somePremissions'], - }; - const newMockReq = { - ...mockReq, - body: { - ...mockReq.body, - roleName: 'some roleName', - presetName: 'some Preset', - permissions: ['test', 'write'], - }, - }; - jest.spyOn(Preset.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); - - const response = await createNewPreset(newMockReq, mockRes); - await flushPromises(); - - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock( - 201, - { - newPreset: data, - message: 'New preset created', - }, - response, - mockRes, - ); - }); - }); - describe('getPresetsByRole method', () => { - test("Ensure getPresetsByRole returns 403 if user doesn't have permissions for putRole", async () => { - const { getPresetsByRole } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(false)); - - const response = await getPresetsByRole(mockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); - - assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); - }); - test('Ensure getPresetsByRole returns 400 if error in finding roleName', async () => { - const { getPresetsByRole } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - roleName: 'test roleName', - }, - }; - jest - .spyOn(Preset, 'find') - .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); - const response = await getPresetsByRole(newMockReq, mockRes); - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock(400, new Error('Error when finding'), response, mockRes); - }); - test('Ensure getPresetsByRole returns 200 if finding roleName successfully', async () => { - const { getPresetsByRole } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - roleName: 'test roleName', - }, - }; - const data = { - roleName: 'test roleName', - presetName: 'test Presetname2', - permissions: ['read', 'add'], - }; - jest.spyOn(Preset, 'find').mockImplementationOnce(() => Promise.resolve(data)); - const response = await getPresetsByRole(newMockReq, mockRes); - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock(200, data, response, mockRes); - }); - }); - describe('updatePresetById method', () => { - test("Ensure updatePresetById returns 403 if user doesn't have permissions for putRole", async () => { - const { updatePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(false)); - - const response = await updatePresetById(mockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); - - assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); - }); - test('Ensure updatePresetById returns 400 if error in finding by id', async () => { - const { updatePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - }; - jest - .spyOn(Preset, 'findById') - .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); - const response = await updatePresetById(newMockReq, mockRes); - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); - }); - test('Ensure updatePresetById returns 400 if error in saving results', async () => { - const { updatePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - body: { - ...mockReq.body, - roleName: 'abc RoleName', - presetName: 'abd Preset', - permissions: ['readABC', 'writeABC'], - }, - }; - const findObj = { save: () => {} }; - const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); - jest.spyOn(findObj, 'save').mockRejectedValue(new Error('Error when saving results')); - const response = await updatePresetById(newMockReq, mockRes); - - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); - assertResMock(400, new Error('Error when saving results'), response, mockRes); - }); - test('Ensure updatePresetById returns 200 if updatePreset by id successfully', async () => { - const { updatePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const data = { - roleName: 'abc RoleName', - presetName: 'abd Preset', - permissions: ['readABC', 'writeABC'], - }; - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - body: { - ...mockReq.body, - roleName: 'abc RoleName', - presetName: 'abd Preset', - permissions: ['readABC', 'writeABC'], - }, - }; - const findObj = { save: () => {} }; - const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); - jest.spyOn(findObj, 'save').mockResolvedValueOnce(data); - const response = await updatePresetById(newMockReq, mockRes); - - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); - assertResMock(200, data, response, mockRes); - }); - }); - describe('deletePresetById method', () => { - test("Ensure deletePresetById returns 403 if user doesn't have permissions for putRole", async () => { - const { deletePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(false)); - - const response = await deletePresetById(mockReq, mockRes); - - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); - - assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); - }); - test('Ensure deletePresetById returns 400 if error in finding by id', async () => { - const { deletePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - }; - jest - .spyOn(Preset, 'findById') - .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); - const response = await deletePresetById(newMockReq, mockRes); - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); - }); - test('Ensure deletePresetById returns 400 if error when removing results', async () => { - const { deletePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - body: { - ...mockReq.body, - roleName: 'abc RoleName', - presetName: 'abd Preset', - permissions: ['readABC', 'writeABC'], - }, - }; - const removeObj = { remove: () => {} }; - const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); - jest.spyOn(removeObj, 'remove').mockRejectedValue({ error: 'Error when removing' }); - const response = await deletePresetById(newMockReq, mockRes); - - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); - assertResMock(400, { error: { error: 'Error when removing' } }, response, mockRes); - }); - test('Ensure deletePresetById returns 200 if deleting successfully', async () => { - const { deletePresetById } = makeSut(); - const hasPermissionSpy = jest - .spyOn(helper, 'hasPermission') - .mockImplementationOnce(() => Promise.resolve(true)); - const newMockReq = { - ...mockReq, - params: { - ...mockReq.params, - presetId: '7237f9af9820a0134ca79c5d', - }, - body: { - ...mockReq.body, - roleName: 'abc RoleName', - presetName: 'abd Preset', - permissions: ['readABC', 'writeABC'], - }, - }; - const removeObj = { remove: () => {} }; - const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); - jest.spyOn(removeObj, 'remove').mockImplementationOnce(() => Promise.resolve(true)); - const response = await deletePresetById(newMockReq, mockRes); - - await flushPromises(); - expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); - - expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); - assertResMock( - 200, - { - message: 'Deleted preset', - }, - response, - mockRes, - ); - }); - }); -}); diff --git a/src/controllers/rolesController.js b/src/controllers/rolesController.js index e81e195c9..90e820b27 100644 --- a/src/controllers/rolesController.js +++ b/src/controllers/rolesController.js @@ -1,23 +1,24 @@ -const UserProfile = require('../models/userProfile'); -const cacheClosure = require('../utilities/nodeCache'); -const { hasPermission } = require('../utilities/permissions'); +const UserProfile = require("../models/userProfile"); +const cache = require("../utilities/nodeCache")(); +const { hasPermission } = require("../utilities/permissions"); const rolesController = function (Role) { - const cache = cacheClosure(); const getAllRoles = function (req, res) { Role.find({}) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send({ error })); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send({ error })); }; const createNewRole = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'postRole'))) { - res.status(403).send('You are not authorized to create new roles.'); + if (!(await hasPermission(req.body.requestor, "postRole"))) { + res.status(403).send("You are not authorized to create new roles."); return; } if (!req.body.roleName || !req.body.permissions) { - res.status(400).send({ error: 'roleName and permissions are mandatory fields.' }); + res + .status(400) + .send({ error: "roleName and permissions are mandatory fields." }); return; } @@ -26,35 +27,34 @@ const rolesController = function (Role) { role.permissions = req.body.permissions; role.permissionsBackEnd = req.body.permissionsBackEnd; - role - .save() - .then((results) => res.status(201).send(results)) - .catch((err) => res.status(500).send({ err })); + role.save().then(results => res.status(201).send(results)).catch(err => res.status(500).send({ err })); }; const getRoleById = function (req, res) { - const { roleId } = req.params; - Role.findById(roleId) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send({ error })); - }; + const { roleId } = req.params; + Role.findById( + roleId, + ) + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send({ error })); +}; const updateRoleById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'putRole'))) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } const { roleId } = req.params; if (!req.body.permissions) { - res.status(400).send({ error: 'Permissions is a mandatory field' }); + res.status(400).send({ error: "Permissions is a mandatory field" }); return; } Role.findById(roleId, (error, record) => { if (error || record === null) { - res.status(400).send('No valid records found'); + res.status(400).send("No valid records found"); return; } @@ -62,39 +62,43 @@ const rolesController = function (Role) { record.permissions = req.body.permissions; record.permissionsBackEnd = req.body.permissionsBackEnd; - record - .save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(400).send(errors)); + record.save() + .then(results => res.status(201).send(results)) + .catch(errors => res.status(400).send(errors)); }); }; const deleteRoleById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'deleteRole'))) { - res.status(403).send('You are not authorized to delete roles.'); + if (!(await hasPermission(req.body.requestor, "deleteRole"))) { + res.status(403).send("You are not authorized to delete roles."); return; } const { roleId } = req.params; - try { - const role = await Role.findById(roleId); - await role.remove(); - await UserProfile.updateMany({ role: role.roleName }, { role: 'Volunteer' }); - - if (cache.hasCache('allusers')) { - const allUserData = JSON.parse(cache.getCache('allusers')); - allUserData.forEach((user) => { - if (user.role === role.roleName) { - user.role = 'Volunteer'; - cache.removeCache(`user-${user._id}`); - } - }); - cache.setCache('allusers', JSON.stringify(allUserData)); - } - res.status(200).send({ message: 'Deleted role' }); - } catch (error) { - res.status(400).send({ error }); - } + Role.findById(roleId) + .then(result => ( + result + .remove() + .then(UserProfile + .updateMany({ role: result.roleName }, { role: 'Volunteer' }) + .then(() => { + const isUserInCache = cache.hasCache('allusers'); + if (isUserInCache) { + const allUserData = JSON.parse(cache.getCache('allusers')); + allUserData.forEach((user) => { + if (user.role === result.roleName) { + user.role = 'Volunteer'; + cache.removeCache(`user-${user._id}`); + } + }); + cache.setCache('allusers', JSON.stringify(allUserData)); + } + res.status(200).send({ message: 'Deleted role' }); + }) + .catch(error => res.status(400).send({ error }))) + .catch(error => res.status(400).send({ error })) + )) + .catch(error => res.status(400).send({ error })); }; return { diff --git a/src/controllers/rolesController.spec.js b/src/controllers/rolesController.spec.js deleted file mode 100644 index eb8b85e2d..000000000 --- a/src/controllers/rolesController.spec.js +++ /dev/null @@ -1,229 +0,0 @@ -const Role = require('../models/role'); -const UserProfile = require('../models/userProfile'); -const { mockReq, mockRes, assertResMock } = require('../test'); - -jest.mock('../models/role'); -jest.mock('../models/userProfile'); -jest.mock('../utilities/permissions'); -jest.mock('../utilities/nodeCache'); - -const cacheClosure = require('../utilities/nodeCache'); -const helper = require('../utilities/permissions'); -const rolesController = require('./rolesController'); - -const flushPromises = () => new Promise(setImmediate); - -const mockHasPermission = (value) => - jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); - -const makeMockCache = (method, value) => { - const cacheObject = { - getCache: jest.fn(), - removeCache: jest.fn(), - hasCache: jest.fn(), - setCache: jest.fn(), - }; - - const mockCache = jest.spyOn(cacheObject, method).mockImplementationOnce(() => value); - - cacheClosure.mockImplementationOnce(() => cacheObject); - - return { mockCache, cacheObject }; -}; -const makeSut = () => { - const { getAllRoles, createNewRole, getRoleById, updateRoleById, deleteRoleById } = - rolesController(Role); - return { getAllRoles, createNewRole, getRoleById, updateRoleById, deleteRoleById }; -}; - -describe('rolesController module', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getAllRoles function', () => { - test('Should return 200 and roles on success', async () => { - const { getAllRoles } = makeSut(); - const mockRoles = [{ roleName: 'role', permission: 'permissionTest' }]; - jest.spyOn(Role, 'find').mockResolvedValue(mockRoles); - const response = await getAllRoles(mockReq, mockRes); - assertResMock(200, mockRoles, response, mockRes); - }); - - test('Should return 404 on error', async () => { - const { getAllRoles } = makeSut(); - const error = new Error('Test Error'); - - jest.spyOn(Role, 'find').mockRejectedValue(error); - const response = await getAllRoles(mockReq, mockRes); - await flushPromises(); - - assertResMock(404, { error }, response, mockRes); - }); - }); - - describe('createNewRole function', () => { - test('Should return 403 if user lacks permission', async () => { - const { createNewRole } = makeSut(); - const hasPermissionSpy = mockHasPermission(false); - const response = await createNewRole(mockReq, mockRes); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postRole'); - assertResMock(403, 'You are not authorized to create new roles.', response, mockRes); - }); - - test('Should return 400 if mandatory fields are missing', async () => { - const { createNewRole } = makeSut(); - mockReq.body = {}; - mockHasPermission(true); - const response = await createNewRole(mockReq, mockRes); - assertResMock( - 400, - { error: 'roleName and permissions are mandatory fields.' }, - response, - mockRes, - ); - }); - - test('Should return 201 and the new role on success', async () => { - const { createNewRole } = makeSut(); - mockHasPermission(true); - mockReq.body = { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }; - const mockRole = { - save: jest.fn().mockResolvedValue({ - roleName: 'newRole', - permissions: ['read'], - permissionsBackEnd: ['write'], - }), - }; - jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); - const response = await createNewRole(mockReq, mockRes); - expect(mockRole.save).toHaveBeenCalled(); - assertResMock( - 201, - { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }, - response, - mockRes, - ); - }); - test('Should return 500 on role save error', async () => { - const { createNewRole } = makeSut(); - mockHasPermission(true); - mockReq.body = { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }; - const mockRole = { save: jest.fn().mockRejectedValue(new Error('Save Error')) }; - jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); - const response = await createNewRole(mockReq, mockRes); - await flushPromises(); - assertResMock(500, { err: new Error('Save Error') }, response, mockRes); - }); - }); - - describe('getRoleById function', () => { - test('Should return 200 and the role on success', async () => { - const { getRoleById } = makeSut(); - const mockRole = { roleName: 'role', permissions: ['read'] }; - jest.spyOn(Role, 'findById').mockResolvedValue(mockRole); - const response = await getRoleById(mockReq, mockRes); - assertResMock(200, mockRole, response, mockRes); - }); - - test('Should return 404 on error', async () => { - const { getRoleById } = makeSut(); - const error = new Error('Test Error'); - jest.spyOn(Role, 'findById').mockRejectedValue(error); - const response = await getRoleById(mockReq, mockRes); - await flushPromises(); - assertResMock(404, { error }, response, mockRes); - }); - }); - - describe('updateRoleById function', () => { - test('Should return 403 if user lacks permission', async () => { - const { updateRoleById } = makeSut(); - const hasPermissionSpy = mockHasPermission(false); - const response = await updateRoleById(mockReq, mockRes); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); - assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); - }); - - test('Should return 400 if mandatory fields are missing', async () => { - const { updateRoleById } = makeSut(); - mockReq.body = {}; - mockHasPermission(true); - const response = await updateRoleById(mockReq, mockRes); - assertResMock(400, { error: 'Permissions is a mandatory field' }, response, mockRes); - }); - - test('Should return 400 if no valid records are found', async () => { - const { updateRoleById } = makeSut(); - mockHasPermission(true); - mockReq.body = { roleId: '5a7e21f00317bc1538def4b7', permissions: ['read'] }; - jest.spyOn(Role, 'findById').mockImplementation((roleId, callback) => callback(null, null)); - const response = await updateRoleById(mockReq, mockRes); - assertResMock(400, 'No valid records found', response, mockRes); - }); - - test('Should return 201 and the updated role on success', async () => { - const { updateRoleById } = makeSut(); - mockHasPermission(true); - mockReq.body = { permissions: ['read'] }; - const mockRole = { - save: jest.fn().mockResolvedValue({ roleName: 'role', permissions: ['read'] }), - }; - jest - .spyOn(Role, 'findById') - .mockImplementation((roleId, callback) => callback(null, mockRole)); - jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); - const response = await updateRoleById(mockReq, mockRes); - expect(mockRole.save).toHaveBeenCalled(); - assertResMock(201, { roleName: 'role', permissions: ['read'] }, response, mockRes); - }); - - test('Should return 500 on role save error', async () => { - const { updateRoleById } = makeSut(); - mockHasPermission(true); - mockReq.body = { permissions: ['read'] }; - const mockRole = { save: jest.fn().mockRejectedValue(new Error('Save Error')) }; - jest - .spyOn(Role, 'findById') - .mockImplementation((roleId, callback) => callback(null, mockRole)); - jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); - const response = await updateRoleById(mockReq, mockRes); - await flushPromises(); - assertResMock(400, new Error('Save Error'), response, mockRes); - }); - }); - - describe('deleteRoleById function', () => { - test('Should return 403 if user lacks permission', async () => { - const { deleteRoleById } = makeSut(); - - const hasPermissionSpy = mockHasPermission(false); - const response = await deleteRoleById(mockReq, mockRes); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteRole'); - assertResMock(403, 'You are not authorized to delete roles.', response, mockRes); - }); - - test('Should return 200 and the deleted role on success', async () => { - mockHasPermission(true); - - const mockRole = { remove: jest.fn().mockResolvedValue(), roleName: 'role' }; - const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - const { deleteRoleById } = makeSut(); - jest - .spyOn(cacheObject, 'getCache') - .mockImplementationOnce(() => JSON.stringify([{ role: 'role', _id: '1' }])); - jest.spyOn(Role, 'findById').mockResolvedValue(mockRole); - jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => {}); - jest.spyOn(cacheObject, 'removeCache').mockImplementationOnce(() => {}); - jest.spyOn(UserProfile, 'updateMany').mockResolvedValue(); - - const response = await deleteRoleById(mockReq, mockRes); - expect(mockRole.remove).toHaveBeenCalled(); - expect(hasCacheMock).toHaveBeenCalledWith('allusers'); - expect(cacheObject.getCache).toHaveBeenCalledWith('allusers'); - expect(cacheObject.setCache).toHaveBeenCalled(); - expect(cacheObject.removeCache).toHaveBeenCalled(); - assertResMock(200, { message: 'Deleted role' }, response, mockRes); - }); - }); -}); diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 1e019ffa1..07ee7b730 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -14,7 +14,6 @@ const taskController = function (Task) { let query = { wbsId: { $in: [req.params.wbsId] }, level: { $in: [level] }, - isActive: { $ne: false }, }; const { mother } = req.params; @@ -28,16 +27,16 @@ const taskController = function (Task) { } Task.find(query) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const getWBSId = (req, res) => { const { wbsId } = req.params; WBS.findById(wbsId) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const updateSumUp = ( @@ -83,7 +82,7 @@ const taskController = function (Task) { }; const calculateSubTasks = (level, tasks) => { - const parentTasks = tasks.filter((task) => task.level === level); + const parentTasks = tasks.filter(task => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let sumHoursBest = 0; @@ -142,7 +141,7 @@ const taskController = function (Task) { }; const setDatesSubTasks = (level, tasks) => { - const parentTasks = tasks.filter((task) => task.level === level); + const parentTasks = tasks.filter(task => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let minStartedDate = task.startedDatetime; @@ -174,7 +173,7 @@ const taskController = function (Task) { }; const calculatePriority = (level, tasks) => { - const parentTasks = tasks.filter((task) => task.level === level); + const parentTasks = tasks.filter(task => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let totalNumberPriority = 0; @@ -216,7 +215,7 @@ const taskController = function (Task) { }; const setAssigned = (level, tasks) => { - const parentTasks = tasks.filter((task) => task.level === level); + const parentTasks = tasks.filter(task => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let isAssigned = false; @@ -249,10 +248,9 @@ const taskController = function (Task) { $and: [ { $or: [{ taskId: parentId1 }, { parentId1 }, { parentId1: null }] }, { wbsId: { $in: [wbsId] } }, - { isActive: { $ne: false } }, ], }).then((tasks) => { - tasks = [...new Set(tasks.map((item) => item))]; + tasks = [...new Set(tasks.map(item => item))]; for (let lv = 3; lv > 0; lv -= 1) { calculateSubTasks(lv, tasks); setDatesSubTasks(lv, tasks); @@ -308,7 +306,7 @@ const taskController = function (Task) { case 3: // task.num is x.x.x, has two levels of parent (parent: x.x and grandparent: x) task.parentId1 = tasksWithId.find((pTask) => pTask.num === taskNumArr[0])._id; // task of parentId1 has num prop of x task.parentId2 = tasksWithId.find( - (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, + pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, )._id; // task of parentId2 has num prop of x.x task.parentId3 = null; task.mother = task.parentId2; // parent task num prop is x.x @@ -316,10 +314,10 @@ const taskController = function (Task) { case 4: // task.num is x.x.x.x, has three levels of parent (x.x.x, x.x and x) task.parentId1 = tasksWithId.find((pTask) => pTask.num === taskNumArr[0])._id; // x task.parentId2 = tasksWithId.find( - (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, + pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, )._id; // x.x task.parentId3 = tasksWithId.find( - (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}.${taskNumArr[2]}`, + pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}.${taskNumArr[2]}`, )._id; // x.x.x task.mother = task.parentId3; // parent task num prop is x.x.x break; @@ -468,7 +466,7 @@ const taskController = function (Task) { }); Promise.all([saveTask, saveWbs, saveProject]) - .then((results) => res.status(201).send(results[0])) + .then(results => res.status(201).send(results[0])) .catch((errors) => { res.status(400).send(errors); }); @@ -492,7 +490,7 @@ const taskController = function (Task) { task .save() .then() - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); }); // level 2 @@ -508,7 +506,7 @@ const taskController = function (Task) { childTask1 .save() .then(true) - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); // level 3 Task.find({ parentId: { $in: [childTask1._id] } }) @@ -523,7 +521,7 @@ const taskController = function (Task) { childTask2 .save() .then(true) - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); // level 4 Task.find({ parentId: { $in: [childTask2._id] } }) @@ -538,19 +536,19 @@ const taskController = function (Task) { childTask3 .save() .then(true) - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); }); } }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }); } }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }); } }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }); res.status(200).send(true); @@ -604,7 +602,7 @@ const taskController = function (Task) { Promise.all(queries) .then(() => res.status(200).send('Success!')) - .catch((err) => res.status(400).send(err)); + .catch(err => res.status(400).send(err)); }); }; @@ -648,7 +646,7 @@ const taskController = function (Task) { Promise.all([removeChildTasks, updateMotherChildrenQty]) .then(() => res.status(200).send({ message: 'Task successfully deleted' })) // no need to resetNum(taskId, mother); - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); }; const deleteTaskByWBS = async (req, res) => { @@ -711,7 +709,7 @@ const taskController = function (Task) { { ...req.body, modifiedDatetime: Date.now() }, ) .then(() => res.status(201).send()) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }; const swap = async function (req, res) { @@ -752,18 +750,18 @@ const taskController = function (Task) { task1 .save() .then() - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); task2 .save() .then() - .catch((errors) => res.status(400).send(errors)); + .catch(errors => res.status(400).send(errors)); Task.find({ wbsId: { $in: [task1.wbsId] }, }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }); }); }; @@ -806,7 +804,7 @@ const taskController = function (Task) { try { Task.find({ wbsId: { $in: [wbsId] } }).then((tasks) => { - tasks = tasks.filter((task) => task.level === 1); + tasks = tasks.filter(task => task.level === 1); tasks.forEach((task) => { updateParents(task.wbsId, task.taskId.toString()); }); @@ -825,54 +823,26 @@ const taskController = function (Task) { const getTasksByUserId = async (req, res) => { const { userId } = req.params; try { - const tasks = await Task.aggregate() - .match({ - resources: { - $elemMatch: { - userID: mongoose.Types.ObjectId(userId), - completedTask: { - $ne: true, - }, - }, - }, - isActive: { - $ne: false, - }, - }) - .lookup({ - from: 'wbs', - localField: 'wbsId', - foreignField: '_id', - as: 'wbs', - }) - .unwind({ - path: '$wbs', - includeArrayIndex: 'string', - preserveNullAndEmptyArrays: true, - }) - .addFields({ - wbsName: '$wbs.wbsName', - projectId: '$wbs.projectId', - }) - .lookup({ - from: 'projects', - localField: 'projectId', - foreignField: '_id', - as: 'project', - }) - .unwind({ - path: '$project', - includeArrayIndex: 'string', - preserveNullAndEmptyArrays: true, - }) - .addFields({ - projectName: '$project.projectName', - }) - .project({ - wbs: 0, - project: 0, + Task.find( + { + 'resources.userID': mongoose.Types.ObjectId(userId), + }, + '-resources.profilePic', + ).then((results) => { + WBS.find({ + _id: { $in: results.map(item => item.wbsId) }, + }).then((WBSs) => { + const resultsWithProjectsIds = results.map((item) => { + item.set( + 'projectId', + WBSs?.find((wbs) => wbs._id.toString() === item.wbsId.toString())?.projectId, + { strict: false }, + ); + return item; + }); + res.status(200).send(resultsWithProjectsIds); }); - res.status(200).send(tasks); + }); } catch (error) { res.status(400).send(error); } @@ -917,7 +887,7 @@ const taskController = function (Task) { { ...req.body, modifiedDatetime: Date.now() }, ) .then(() => res.status(201).send()) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }; const getReviewReqEmailBody = function (name, taskName) { @@ -937,7 +907,7 @@ const taskController = function (Task) { role: { $in: ['Administrator', 'Manager', 'Mentor'] }, }); membership.forEach((member) => { - if (member.teams.some((team) => user.teams.includes(team))) { + if (member.teams.some(team => user.teams.includes(team))) { recipients.push(member.email); } }); diff --git a/src/controllers/taskEditSuggestionController.js b/src/controllers/taskEditSuggestionController.js index 4f9bd6de8..721979d4b 100644 --- a/src/controllers/taskEditSuggestionController.js +++ b/src/controllers/taskEditSuggestionController.js @@ -5,18 +5,9 @@ const wbs = require('../models/wbs'); const taskEditSuggestionController = function (TaskEditSuggestion) { const createOrUpdateTaskEditSuggestion = async function (req, res) { try { - const profile = await userProfile - .findById(mongoose.Types.ObjectId(req.body.userId)) - .select('firstName lastName'); - const wbsProjectId = await wbs - .findById(mongoose.Types.ObjectId(req.body.oldTask.wbsId)) - .select('projectId'); - const projectMembers = await userProfile - .find( - { projects: mongoose.Types.ObjectId(wbsProjectId.projectId) }, - '_id firstName lastName profilePic', - ) - .sort({ firstName: 1, lastName: 1 }); + const profile = await userProfile.findById(mongoose.Types.ObjectId(req.body.userId)).select('firstName lastName'); + const wbsProjectId = await wbs.findById(mongoose.Types.ObjectId(req.body.oldTask.wbsId)).select('projectId'); + const projectMembers = await userProfile.find({ projects: mongoose.Types.ObjectId(wbsProjectId.projectId) }, '_id firstName lastName profilePic').sort({ firstName: 1, lastName: 1 }); const taskIdQuery = { taskId: mongoose.Types.ObjectId(req.body.taskId) }; const update = { @@ -31,10 +22,7 @@ const taskEditSuggestionController = function (TaskEditSuggestion) { projectMembers, }; const options = { - upsert: true, - new: true, - setDefaultsOnInsert: true, - rawResult: true, + upsert: true, new: true, setDefaultsOnInsert: true, rawResult: true, }; const tes = await TaskEditSuggestion.findOneAndUpdate(taskIdQuery, update, options); res.status(200).send(tes); @@ -59,16 +47,8 @@ const taskEditSuggestionController = function (TaskEditSuggestion) { const deleteTaskEditSuggestion = async function (req, res) { try { - const result = await TaskEditSuggestion.deleteOne(req.param.taskEditSuggestionId); - if (result.deletedCount === 1) { - res.status(200).send({ - message: `Deleted task edit suggestion with _id: ${req.param.taskEditSuggestionId}`, - }); - } else { - res.status(400).send({ - message: `Failed to delete task edit suggestion with _id: ${req.param.taskEditSuggestionId}`, - }); - } + await TaskEditSuggestion.deleteOne(req.param.taskEditSuggestionId); + res.status(200).send({ message: `Deleted task edit suggestion with _id: ${req.param.taskEditSuggestionId}` }); } catch (error) { res.status(400).send(error); } diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 41f515e99..fb11120bc 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -228,97 +228,15 @@ const teamcontroller = function (Team) { res.status(500).send(error); }); }; - const updateTeamVisibility = async (req, res) => { - console.log("==============> 9 "); - const { visibility, teamId, userId } = req.body; - - try { - Team.findById(teamId, (error, team) => { - if (error || team === null) { - res.status(400).send('No valid records found'); - return; - } - - const memberIndex = team.members.findIndex(member => member.userId.toString() === userId); - if (memberIndex === -1) { - res.status(400).send('Member not found in the team.'); - return; - } - - team.members[memberIndex].visible = visibility; - team.modifiedDatetime = Date.now(); - - team.save() - .then(updatedTeam => { - // Additional operations after team.save() - const assignlist = []; - const unassignlist = []; - team.members.forEach(member => { - if (member.userId.toString() === userId) { - // Current user, no need to process further - return; - } - - if (visibility) { - assignlist.push(member.userId); - } else { - console.log("Visiblity set to false so removing it"); - unassignlist.push(member.userId); - } - }); - - const addTeamToUserProfile = userProfile - .updateMany({ _id: { $in: assignlist } }, { $addToSet: { teams: teamId } }) - .exec(); - const removeTeamFromUserProfile = userProfile - .updateMany({ _id: { $in: unassignlist } }, { $pull: { teams: teamId } }) - .exec(); - - Promise.all([addTeamToUserProfile, removeTeamFromUserProfile]) - .then(() => { - res.status(200).send({ result: 'Done' }); - }) - .catch((error) => { - res.status(500).send({ error }); - }); - }) - .catch(errors => { - console.error('Error saving team:', errors); - res.status(400).send(errors); - }); - - }); - } catch (error) { - res.status(500).send(`Error updating team visibility: ${ error.message}`); - } - }; - - /** - * Leaner version of the teamcontroller.getAllTeams - * Remove redundant data: members, isActive, createdDatetime, modifiedDatetime. - */ - const getAllTeamCode = async function (req, res) { - Team.find({ isActive: true }, { teamCode: 1, _id: 1, teamName: 1 }) - .then((results) => { - res.status(200).send(results); - }) - .catch((error) => { - // logger.logException(`Fetch team code failed: ${error}`); - res.status(500).send('Fetch team code failed.'); - }); - }; - return { getAllTeams, - getAllTeamCode, getTeamById, postTeam, deleteTeam, putTeam, assignTeamToUsers, getTeamMembership, - updateTeamVisibility, }; }; diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 5a9b8e973..4d90d6613 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1,6 +1,7 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const logger = require('../startup/logger'); +const { getInfringementEmailBody } = require('../helpers/userHelper')(); const UserProfile = require('../models/userProfile'); const Project = require('../models/project'); const Task = require('../models/task'); @@ -9,6 +10,7 @@ const emailSender = require('../utilities/emailSender'); const { hasPermission } = require('../utilities/permissions'); const cacheClosure = require('../utilities/nodeCache'); + const formatSeconds = function (seconds) { const formattedseconds = parseInt(seconds, 10); const values = `${Math.floor( @@ -322,83 +324,13 @@ const addEditHistory = async ( (edit) => moment().tz('America/Los_Angeles').diff(edit.date, 'days') <= 365, ).length; - if (totalRecentEdits >= 5) { - const cutOffDate = moment().subtract(1, 'year'); - const recentInfringements = userprofile.infringements.filter((infringement) => - moment(infringement.date).isAfter(cutOffDate), - ); - let modifiedRecentInfringements = 'No Previous Infringements!'; - if (recentInfringements.length) { - modifiedRecentInfringements = recentInfringements - .map((item, index) => { - let enhancedDescription; - if (item.description) { - let sentences = item.description.split('.'); - const dateRegex = - /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; - sentences = sentences.map((sentence) => - sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { - const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; - }), - ); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes( - 'System auto-assigned infringement for editing your time entries', - ) - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - enhancedDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - enhancedDescription = `${item.description}`; - } - } - return `

${index + 1}. Date: ${moment(item.date).format( - 'M-D-YYYY', - )}, Description: ${enhancedDescription}

`; - }) - .join(''); - } - + if (totalRecentEdits >= 3) { userprofile.infringements.push({ date: moment().tz('America/Los_Angeles'), - description: `System auto-assigned infringement for editing your time entries <${totalRecentEdits}> times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty. - time entry edits in the last calendar year`, + description: `${totalRecentEdits} time entry edits in the last calendar year`, }); - const infringementNotificationToAdminEmailBody = ` + const infringementNotificationEmail = `

${userprofile.firstName} ${userprofile.lastName} (${userprofile.email}) was issued a blue square for editing their time entries ${totalRecentEdits} times within the last calendar year. @@ -408,37 +340,28 @@ const addEditHistory = async (

`; - const infringementNotificationToUserEmailBody = `Dear ${userprofile.firstName} ${userprofile.lastName}, -

Oops, it looks like you chose to edit your time entries too many times and you’ve managed to get a blue square.

-

Date Assigned: ${moment().tz('America/Los_Angeles').format('M-D-YYYY')}

\ -

Description: System auto-assigned infringement for editing your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.

-

Total Infringements: This is your ${moment - .localeData() - .ordinal(recentInfringements.length)} blue square of 5.

-

Thank you,

-

One Community

- -         -
-

ADMINISTRATIVE DETAILS:

-

Start Date: ${moment(userprofile.startDate).utc().format('M-D-YYYY')}

-

Role: ${userprofile.role}

-

Title: ${userprofile.userTitle || 'Volunteer'}

-

Previous Blue Square Reasons:

- ${modifiedRecentInfringements}`; + const emailInfringement = { + date: moment().tz('America/Los_Angeles').format('MMMM-DD-YY'), + description: `You edited your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.`, + }; pendingEmailCollection.push( emailSender.bind( null, 'onecommunityglobal@gmail.com', `${userprofile.firstName} ${userprofile.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, - infringementNotificationToAdminEmailBody, + infringementNotificationEmail, ), emailSender.bind( null, userprofile.email, - 'You’ve been issued a blue square for editing your time entries too many times', - infringementNotificationToUserEmailBody, + "You've been issued a blue square for editing your time entry", + getInfringementEmailBody( + userprofile.firstName, + userprofile.lastName, + emailInfringement, + userprofile.infringements.length, + ), ), ); } @@ -466,29 +389,34 @@ const updateTaskIdInTimeEntry = async (id, timeEntry) => { Object.assign(timeEntry, { taskId, wbsId, projectId }); }; + + /** * Controller for timeEntry */ const timeEntrycontroller = function (TimeEntry) { + /** - * Helper func: Check if this is the first time entry for the given user id - * - * @param {Mongoose.ObjectId} personId - * @returns - */ - const checkIsUserFirstTimeEntry = async (personId) => { - try { - const timeEntry = await TimeEntry.findOne({ - personId, - }); - if (timeEntry) { - return false; - } - } catch (error) { - throw new Error(`Failed to check user with id ${personId} on time entry`); + * Helper func: Check if this is the first time entry for the given user id + * + * @param {Mongoose.ObjectId} personId + * @returns + */ +const checkIsUserFirstTimeEntry = async (personId) => { + try { + const timeEntry = await TimeEntry.findOne({ + personId, + }); + if (timeEntry) { + return false; } - return true; - }; + } catch (error) { + throw new Error( + `Failed to check user with id ${personId} on time entry`, + ); + } + return true; +}; /** * Post a time entry @@ -503,13 +431,6 @@ const timeEntrycontroller = function (TimeEntry) { result.status(400).send({ error: 'Bad request' }); }; - const isPostingForSelf = req.body.personId === req.body.requestor.requestorId; - const canPostTimeEntriesForOthers = await hasPermission(req.body.requestor, 'postTimeEntry'); - if (!isPostingForSelf && !canPostTimeEntriesForOthers) { - res.status(403).send({ error: 'You do not have permission to post time entries for others' }); - return; - } - switch (req.body.entryType) { case 'person': if (!mongoose.Types.ObjectId.isValid(req.body.personId) || isInvalid) returnErr(res); @@ -553,50 +474,47 @@ const timeEntrycontroller = function (TimeEntry) { const userprofile = await UserProfile.findById(timeEntry.personId); - if (userprofile) { - // if the time entry is tangible, update the tangible hours in the user profile - if (timeEntry.isTangible) { - // update the total tangible hours in the user profile and the hours by category - updateUserprofileTangibleIntangibleHrs(timeEntry.totalSeconds, 0, userprofile); - updateUserprofileCategoryHrs( - null, - null, - timeEntry.projectId, + // if the time entry is tangible, update the tangible hours in the user profile + if (timeEntry.isTangible) { + // update the total tangible hours in the user profile and the hours by category + updateUserprofileTangibleIntangibleHrs(timeEntry.totalSeconds, 0, userprofile); + updateUserprofileCategoryHrs( + null, + null, + timeEntry.projectId, + timeEntry.totalSeconds, + userprofile, + ); + // if the time entry is related to a task, update the task hoursLogged + if (timeEntry.taskId) { + updateTaskLoggedHours( + timeEntry.taskId, + 0, + timeEntry.taskId, timeEntry.totalSeconds, userprofile, + session, + pendingEmailCollection, ); - // if the time entry is related to a task, update the task hoursLogged - if (timeEntry.taskId) { - updateTaskLoggedHours( - timeEntry.taskId, - 0, - timeEntry.taskId, - timeEntry.totalSeconds, - userprofile, - session, - pendingEmailCollection, - ); - } - } else { - // if the time entry is intangible, just update the intangible hours in the userprofile - updateUserprofileTangibleIntangibleHrs(0, timeEntry.totalSeconds, userprofile); } + } else { + // if the time entry is intangible, just update the intangible hours in the userprofile + updateUserprofileTangibleIntangibleHrs(0, timeEntry.totalSeconds, userprofile); } // Replace the isFirstTimelog checking logic from the frontend to the backend // Update the user start date to current date if this is the first time entry (Weekly blue square assignment related) const isFirstTimeEntry = await checkIsUserFirstTimeEntry(timeEntry.personId); - if (isFirstTimeEntry) { + if(isFirstTimeEntry) { userprofile.isFirstTimelog = false; userprofile.startDate = now; } await timeEntry.save({ session }); - if (userprofile) { - await userprofile.save({ session }); - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); - } + await userprofile.save({ session }); + + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); await session.commitTransaction(); pendingEmailCollection.forEach((emailHandler) => emailHandler()); @@ -651,6 +569,16 @@ const timeEntrycontroller = function (TimeEntry) { const isSameDayTimeEntry = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === newDateOfWork; const isSameDayAuthUserEdit = isForAuthUser && isSameDayTimeEntry; + const isRequestorAdminLikeRole = ['Owner', 'Administrator'].includes(req.body.requestor.role); + const hasEditTimeEntryPermission = await hasPermission(req.body.requestor, 'editTimeEntry'); + + const canEdit = isSameDayAuthUserEdit || isRequestorAdminLikeRole || hasEditTimeEntryPermission; + + + if (!canEdit) { + const error = 'Unauthorized request'; + return res.status(403).send({ error }); + } const session = await mongoose.startSession(); session.startTransaction(); @@ -688,7 +616,7 @@ const timeEntrycontroller = function (TimeEntry) { dateOfWork: initialDateOfWork, } = timeEntry; - const initialProjectId = initialProjectIdObject ? initialProjectIdObject.toString() : null; + const initialProjectId = initialProjectIdObject.toString(); const initialTaskId = initialTaskIdObject ? initialTaskIdObject.toString() : null; // Check if any of the fields have changed @@ -696,61 +624,11 @@ const timeEntrycontroller = function (TimeEntry) { const tangibilityChanged = initialIsTangible !== newIsTangible; const timeChanged = initialTotalSeconds !== newTotalSeconds; const dateOfWorkChanged = initialDateOfWork !== newDateOfWork; - const isTimeModified = newTotalSeconds !== timeEntry.totalSeconds; - const isDescriptionModified = newNotes !== timeEntry.notes; - - - const canEditTimeEntryTime = await hasPermission(req.body.requestor, 'editTimeEntryTime'); - const canEditTimeEntryDescription = await hasPermission(req.body.requestor, 'editTimeEntryDescription'); - const canEditTimeEntryDate = await hasPermission(req.body.requestor, 'editTimeEntryDate'); - const canEditTimeEntryIsTangible = (isForAuthUser - ? (await hasPermission(req.body.requestor, 'toggleTangibleTime')) - : (await hasPermission(req.body.requestor, 'editTimeEntryToggleTangible'))); - - const isNotUsingAPermission = - (!canEditTimeEntryTime && isTimeModified) || - (!canEditTimeEntryDate && dateOfWorkChanged); - - // Time - if ( - !isSameDayAuthUserEdit && - isTimeModified && - !canEditTimeEntryTime - ) { - const error = `You do not have permission to edit the time entry time`; - return res.status(403).send({ error }); - } - - // Description - if ( - !isSameDayAuthUserEdit && - isDescriptionModified && - !canEditTimeEntryDescription - ) { - const error = `You do not have permission to edit the time entry description`; - return res.status(403).send({ error }); - } - - // Date - if (dateOfWorkChanged && !canEditTimeEntryDate) { - const error = `You do not have permission to edit the time entry date`; - return res.status(403).send({ error }); - } - - // Tangible Time - if ( - tangibilityChanged && - canEditTimeEntryIsTangible - ) { - const error = `You do not have permission to edit the time entry isTangible`; - return res.status(403).send({ error }); - } - timeEntry.notes = newNotes; timeEntry.totalSeconds = newTotalSeconds; timeEntry.isTangible = newIsTangible; timeEntry.lastModifiedDateTime = moment().utc().toISOString(); - if (newProjectId) timeEntry.projectId = mongoose.Types.ObjectId(newProjectId); + timeEntry.projectId = mongoose.Types.ObjectId(newProjectId); timeEntry.wbsId = newWbsId ? mongoose.Types.ObjectId(newWbsId) : null; timeEntry.taskId = newTaskId ? mongoose.Types.ObjectId(newTaskId) : null; timeEntry.dateOfWork = moment(newDateOfWork).format('YYYY-MM-DD'); @@ -758,128 +636,129 @@ const timeEntrycontroller = function (TimeEntry) { // now handle the side effects in task and userprofile if certain fields have changed const userprofile = await UserProfile.findById(personId); - if (userprofile) { - if (tangibilityChanged) { - // if tangibility changed - // tangiblity change usually only happens by itself via tangibility checkbox, - // and it can't be changed by user directly (except for owner-like roles) - // but here the other changes are also considered here for completeness - // change from tangible to intangible - if (initialIsTangible) { - // subtract initial logged hours from old task (if not null) - updateTaskLoggedHours( - initialTaskId, - initialTotalSeconds, - null, - null, - userprofile, - session, - pendingEmailCollection, - ); - // subtract initial logged hours from userprofile totalTangibleHrs and add new logged hours to userprofile totalIntangibleHrs - updateUserprofileTangibleIntangibleHrs( - -initialTotalSeconds, - newTotalSeconds, - userprofile, - ); + if (tangibilityChanged) { + // if tangibility changed + // tangiblity change usually only happens by itself via tangibility checkbox, + // and it can't be changed by user directly (except for owner-like roles) + // but here the other changes are also considered here for completeness + // change from tangible to intangible + if (initialIsTangible) { + // subtract initial logged hours from old task (if not null) + updateTaskLoggedHours( + initialTaskId, + initialTotalSeconds, + null, + null, + userprofile, + session, + pendingEmailCollection, + ); + // subtract initial logged hours from userprofile totalTangibleHrs and add new logged hours to userprofile totalIntangibleHrs + updateUserprofileTangibleIntangibleHrs( + -initialTotalSeconds, + newTotalSeconds, + userprofile, + ); - // if project is changed, update userprofile hoursByCategory - if (projectChanged) { - updateUserprofileCategoryHrs( - initialProjectIdObject, - initialTotalSeconds, - null, - null, - userprofile, - ); - } - } else { - // from intangible to tangible - updateTaskLoggedHours( + // if project is changed, update userprofile hoursByCategory + if (projectChanged) { + updateUserprofileCategoryHrs( + initialProjectIdObject, + initialTotalSeconds, null, null, - newTaskId, - newTotalSeconds, - userprofile, - session, - pendingEmailCollection, - ); - updateUserprofileTangibleIntangibleHrs( - initialTotalSeconds, - -newTotalSeconds, userprofile, ); - if (projectChanged) { - updateUserprofileCategoryHrs(null, null, newProjectId, newTotalSeconds, userprofile); - } } - // make sure all hours are positive - validateUserprofileHours(userprofile); - } else if (initialIsTangible) { - // if tangibility is not changed, - // when timeentry remains tangible, this is usually when timeentry is edited by user in the same day or by owner-like roles - - // it doesn't matter if task is changed or not, just update taskLoggedHours and userprofile totalTangibleHours with new and old task ids + } else { + // from intangible to tangible updateTaskLoggedHours( - initialTaskId, - initialTotalSeconds, + null, + null, newTaskId, newTotalSeconds, userprofile, session, pendingEmailCollection, ); - // when project is also changed + updateUserprofileTangibleIntangibleHrs( + initialTotalSeconds, + -newTotalSeconds, + userprofile, + ); if (projectChanged) { - updateUserprofileCategoryHrs( - initialProjectIdObject, - initialTotalSeconds, - newProjectId, - newTotalSeconds, - userprofile, - ); - validateUserprofileHours(userprofile); + updateUserprofileCategoryHrs(null, null, newProjectId, newTotalSeconds, userprofile); } - // if time or dateOfWork is changed - if (timeChanged || dateOfWorkChanged) { - const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; - updateUserprofileTangibleIntangibleHrs(timeDiffInSeconds, 0, userprofile); - notifyEditByEmail( + } + // make sure all hours are positive + validateUserprofileHours(userprofile); + } else if (initialIsTangible) { + // if tangibility is not changed, + // when timeentry remains tangible, this is usually when timeentry is edited by user in the same day or by owner-like roles + + // it doesn't matter if task is changed or not, just update taskLoggedHours and userprofile totalTangibleHours with new and old task ids + updateTaskLoggedHours( + initialTaskId, + initialTotalSeconds, + newTaskId, + newTotalSeconds, + userprofile, + session, + pendingEmailCollection, + ); + // when project is also changed + if (projectChanged) { + updateUserprofileCategoryHrs( + initialProjectIdObject, + initialTotalSeconds, + newProjectId, + newTotalSeconds, + userprofile, + ); + validateUserprofileHours(userprofile); + } + // if time or dateOfWork is changed + if (timeChanged || dateOfWorkChanged) { + const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; + updateUserprofileTangibleIntangibleHrs(timeDiffInSeconds, 0, userprofile); + notifyEditByEmail( + userprofile, + req.body.requestor.requestorId, + initialTotalSeconds, + newTotalSeconds, + initialDateOfWork, + newDateOfWork, + ); + // Update edit history + if ( + !isRequestorAdminLikeRole && + !hasEditTimeEntryPermission && + isSameDayAuthUserEdit && + isGeneralEntry + ) { + addEditHistory( userprofile, - req.body.requestor.requestorId, initialTotalSeconds, newTotalSeconds, initialDateOfWork, newDateOfWork, + pendingEmailCollection, ); - // Update edit history - if (isNotUsingAPermission && isSameDayAuthUserEdit && isGeneralEntry) { - addEditHistory( - userprofile, - initialTotalSeconds, - newTotalSeconds, - initialDateOfWork, - newDateOfWork, - pendingEmailCollection, - ); - } } - } else { - // when timeentry is intangible before and after change, - // just update timeEntry and the intangible hours in userprofile, - // no need to update task/userprofile - const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; - updateUserprofileTangibleIntangibleHrs(0, timeDiffInSeconds, userprofile); } + } else { + // when timeentry is intangible before and after change, + // just update timeEntry and the intangible hours in userprofile, + // no need to update task/userprofile + const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; + updateUserprofileTangibleIntangibleHrs(0, timeDiffInSeconds, userprofile); } await timeEntry.save({ session }); - if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session }); - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); - } + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); pendingEmailCollection.forEach((emailHandler) => emailHandler()); await session.commitTransaction(); @@ -914,17 +793,17 @@ const timeEntrycontroller = function (TimeEntry) { } const { personId, totalSeconds, dateOfWork, projectId, taskId, isTangible } = timeEntry; - const isForAuthUser = personId - ? personId.toString() === req.body.requestor.requestorId - : false; + const isForAuthUser = personId.toString() === req.body.requestor.requestorId; const isSameDayTimeEntry = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === dateOfWork; const isSameDayAuthUserDelete = isForAuthUser && isSameDayTimeEntry; + const isRequestorAdminLikeRole = ['Owner', 'Administrator'].includes(req.body.requestor.role); const hasDeleteTimeEntryPermission = await hasPermission( req.body.requestor, 'deleteTimeEntry', ); - const canDelete = isSameDayAuthUserDelete || hasDeleteTimeEntryPermission; + const canDelete = + isSameDayAuthUserDelete || isRequestorAdminLikeRole || hasDeleteTimeEntryPermission; if (!canDelete) { res.status(403).send({ error: 'Unauthorized request' }); return; @@ -932,27 +811,23 @@ const timeEntrycontroller = function (TimeEntry) { const userprofile = await UserProfile.findById(personId); - if (userprofile) { - // Revert this tangible timeEntry of related task's hoursLogged - if (isTangible) { - updateUserprofileTangibleIntangibleHrs(-totalSeconds, 0, userprofile); - updateUserprofileCategoryHrs(projectId, totalSeconds, null, null, userprofile); - // if the time entry is related to a task, update the task hoursLogged - if (taskId) { - updateTaskLoggedHours(taskId, totalSeconds, null, null, userprofile, session); - } - } else { - updateUserprofileTangibleIntangibleHrs(0, -totalSeconds, userprofile); + // Revert this tangible timeEntry of related task's hoursLogged + if (isTangible) { + updateUserprofileTangibleIntangibleHrs(-totalSeconds, 0, userprofile); + updateUserprofileCategoryHrs(projectId, totalSeconds, null, null, userprofile); + // if the time entry is related to a task, update the task hoursLogged + if (taskId) { + updateTaskLoggedHours(taskId, totalSeconds, null, null, userprofile, session); } + } else { + updateUserprofileTangibleIntangibleHrs(0, -totalSeconds, userprofile); } + await userprofile.save({ session }); await timeEntry.remove({ session }); - if (userprofile) { - await userprofile.save({ session }); - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); - } + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); await session.commitTransaction(); res.status(200).send({ message: 'Successfully deleted' }); @@ -990,7 +865,6 @@ const timeEntrycontroller = function (TimeEntry) { entryType: { $in: ['default', null] }, personId: userId, dateOfWork: { $gte: fromdate, $lte: todate }, - isActive: { $ne: false }, }).sort('-lastModifiedDateTime'); const results = await Promise.all( @@ -998,18 +872,6 @@ const timeEntrycontroller = function (TimeEntry) { timeEntry = { ...timeEntry.toObject() }; const { projectId, taskId } = timeEntry; if (!taskId) await updateTaskIdInTimeEntry(projectId, timeEntry); // if no taskId, then it might be old time entry data that didn't separate projectId with taskId - if (timeEntry.taskId) { - const task = await Task.findById(timeEntry.taskId); - if (task) { - timeEntry.taskName = task.taskName; - } - } - if (timeEntry.projectId) { - const project = await Project.findById(timeEntry.projectId); - if (project) { - timeEntry.projectName = project.projectName; - } - } const hours = Math.floor(timeEntry.totalSeconds / 3600); const minutes = Math.floor((timeEntry.totalSeconds % 3600) / 60); Object.assign(timeEntry, { hours, minutes, totalSeconds: undefined }); @@ -1035,7 +897,7 @@ const timeEntrycontroller = function (TimeEntry) { personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, - '-createdDateTime', + ' -createdDateTime', ) .populate('personId') .populate('projectId') @@ -1044,6 +906,7 @@ const timeEntrycontroller = function (TimeEntry) { .sort({ lastModifiedDateTime: -1 }) .then((results) => { const data = []; + results.forEach((element) => { const record = {}; record._id = element._id; @@ -1053,48 +916,15 @@ const timeEntrycontroller = function (TimeEntry) { record.userProfile = element.personId; record.dateOfWork = element.dateOfWork; [record.hours, record.minutes] = formatSeconds(element.totalSeconds); - record.projectId = element.projectId?._id || null; - record.projectName = element.projectId?.projectName || null; - record.projectCategory = element.projectId?.category.toLowerCase() || null; + record.projectId = element.projectId._id; + record.projectName = element.projectId.projectName; + record.projectCategory = element.projectId.category.toLowerCase(); record.taskId = element.taskId?._id || null; record.taskName = element.taskId?.taskName || null; record.taskClassification = element.taskId?.classification?.toLowerCase() || null; record.wbsId = element.wbsId?._id || null; record.wbsName = element.wbsId?.wbsName || null; - data.push(record); - }); - res.status(200).send(data); - }) - .catch((error) => { - logger.logException(error); - res.status(400).send(error); - }); - }; - const getTimeEntriesForReports = function (req, res) { - const { users, fromDate, toDate } = req.body; - - TimeEntry.find( - { - personId: { $in: users }, - dateOfWork: { $gte: fromDate, $lte: toDate }, - }, - ' -createdDateTime', - ) - .populate('projectId') - - .then((results) => { - const data = []; - - results.forEach((element) => { - const record = {}; - record._id = element._id; - record.isTangible = element.isTangible; - record.personId = element.personId._id; - record.dateOfWork = element.dateOfWork; - [record.hours, record.minutes] = formatSeconds(element.totalSeconds); - record.projectId = element.projectId ? element.projectId._id : ''; - record.projectName = element.projectId ? element.projectId.projectName : ''; data.push(record); }); @@ -1120,11 +950,10 @@ const timeEntrycontroller = function (TimeEntry) { { projectId, dateOfWork: { $gte: fromDate, $lte: todate }, - isActive: { $ne: false }, }, '-createdDateTime -lastModifiedDateTime', ) - .populate('personId', 'firstName lastName isActive') + .populate('userId') .sort({ dateOfWork: -1 }) .then((results) => { res.status(200).send(results); @@ -1145,7 +974,6 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'person', personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, - isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1185,7 +1013,6 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'project', projectId: { $in: projects }, dateOfWork: { $gte: fromDate, $lte: toDate }, - isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1223,7 +1050,6 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'team', teamId: { $in: teams }, dateOfWork: { $gte: fromDate, $lte: toDate }, - isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1260,7 +1086,6 @@ const timeEntrycontroller = function (TimeEntry) { getLostTimeEntriesForUserList, getLostTimeEntriesForProjectList, getLostTimeEntriesForTeamList, - getTimeEntriesForReports, }; }; diff --git a/src/controllers/timeOffRequestController.spec.js b/src/controllers/timeOffRequestController.spec.js deleted file mode 100644 index 9f82c5492..000000000 --- a/src/controllers/timeOffRequestController.spec.js +++ /dev/null @@ -1,1260 +0,0 @@ -jest.mock('../utilities/permissions', () => ({ - hasPermission: jest.fn(), // Mocking the hasPermission function directly -})); -jest.mock('../utilities/emailSender'); - -const mongoose = require('mongoose'); -const moment = require('moment-timezone'); -const emailSender = require('../utilities/emailSender'); -const { hasPermission } = require('../utilities/permissions'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const timeOffRequestController = require('./timeOffRequestController'); -const TimeOffRequest = require('../models/timeOffRequest'); -const Team = require('../models/team'); -const UserProfile = require('../models/userProfile'); - -const flushPromises = () => new Promise(setImmediate); - -const { ObjectId } = mongoose.Types; - -const makeSut = () => { - const { - setTimeOffRequest, - getTimeOffRequests, - getTimeOffRequestbyId, - updateTimeOffRequestById, - deleteTimeOffRequestById, - } = timeOffRequestController(TimeOffRequest, Team, UserProfile); - return { - setTimeOffRequest, - getTimeOffRequests, - getTimeOffRequestbyId, - updateTimeOffRequestById, - deleteTimeOffRequestById, - }; -}; - -const getAdminEmailIds = (userProfiles) => { - const rolesToInclude = ['Manager', 'Mentor', 'Administrator']; // describes Admin roles - - return userProfiles - .map((userProfile) => { - if (rolesToInclude.includes(userProfile.role)) { - return userProfile.email; - } - return null; - }) - .filter((email) => email !== null); -}; - -describe('timeOffRequestController.js module', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getTimeOffRequests function', () => { - test('getTimeOffRequests Returns 200 and correctly formatter all time-off requests', async () => { - const { getTimeOffRequests } = makeSut(); - const mockData = [ - { - requestFor: '60c72b2f5f1b2c001c8e4d67', - requests: [ - { - reason: 'Vacation', - startingDate: '2024-06-01T00:00:00Z', - endingDate: '2024-06-07T00:00:00Z', - duration: 1, - }, - { - reason: 'Family Event', - startingDate: '2024-06-15T00:00:00Z', - endingDate: '2024-06-16T00:00:00Z', - duration: 1, - }, - ], - }, - { - requestFor: '60c72b2f5f1b2c001c8e4d68', - requests: [ - { - reason: 'Sick Leave', - startingDate: '2024-06-02T00:00:00Z', - endingDate: '2024-06-13T00:00:00Z', - duration: 2, - }, - ], - }, - { - requestFor: '60c72b2f5f1b2c001c8e4d69', - requests: [ - { - reason: 'Conference', - startingDate: '2024-06-05T00:00:00Z', - endingDate: '2024-06-28T00:00:00Z', - duration: 4, - }, - ], - }, - ]; - - const timeOffRequestAggregateSpy = jest - .spyOn(TimeOffRequest, 'aggregate') - .mockResolvedValueOnce(mockData); - - const expectedFormattedMockData = { - '60c72b2f5f1b2c001c8e4d67': [ - { - reason: 'Vacation', - startingDate: '2024-06-01T00:00:00Z', - endingDate: '2024-06-07T00:00:00Z', - duration: 1, - }, - { - reason: 'Family Event', - startingDate: '2024-06-15T00:00:00Z', - endingDate: '2024-06-16T00:00:00Z', - duration: 1, - }, - ], - '60c72b2f5f1b2c001c8e4d68': [ - { - reason: 'Sick Leave', - startingDate: '2024-06-02T00:00:00Z', - endingDate: '2024-06-13T00:00:00Z', - duration: 2, - }, - ], - '60c72b2f5f1b2c001c8e4d69': [ - { - reason: 'Conference', - startingDate: '2024-06-05T00:00:00Z', - endingDate: '2024-06-28T00:00:00Z', - duration: 4, - }, - ], - }; - - const response = await getTimeOffRequests(mockReq, mockRes); - await flushPromises(); - - assertResMock(200, expectedFormattedMockData, response, mockRes); - expect(timeOffRequestAggregateSpy).toHaveBeenCalled(); - expect(timeOffRequestAggregateSpy).toHaveBeenCalledTimes(1); - }); - - test('getTimeOffRequests Returns 500 if error encountered while aggregating all time-off requests', async () => { - const { getTimeOffRequests } = makeSut(); - const error = { error: 'Error perforing aggregate operation.' }; - const timeOffRequestAggregateSpy = jest - .spyOn(TimeOffRequest, 'aggregate') - .mockRejectedValueOnce(error); - - const response = await getTimeOffRequests(mockReq, mockRes); - await flushPromises(); - - assertResMock(500, error, response, mockRes); - expect(timeOffRequestAggregateSpy).toHaveBeenCalled(); - expect(timeOffRequestAggregateSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('getTimeOffRequestbyId function', () => { - test('Returns 404 if time-off request with a particular id is not found', async () => { - const { getTimeOffRequestbyId } = makeSut(); - const mockData = null; - - const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockResolvedValueOnce(mockData); - - const response = await getTimeOffRequestbyId(mockReq, mockRes); - await flushPromises(); - const error = 'Time off request not found'; - assertResMock(404, error, response, mockRes); - expect(findOneSpy).toHaveBeenCalled(); - expect(findOneSpy).toHaveBeenCalledTimes(1); - }); - - test('Returns 200 if time-off request with a particular id is found', async () => { - const { getTimeOffRequestbyId } = makeSut(); - const mockData = { - requestFor: 'sd9028_sdas83ink84haso1', - reason: 'Family Gathering.', - startingDate: new Date(2024, 5, 1), - endingDate: new Date(2024, 5, 13), - duration: 2, - }; - - const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockResolvedValueOnce(mockData); - - const response = await getTimeOffRequestbyId(mockReq, mockRes); - await flushPromises(); - - assertResMock(200, mockData, response, mockRes); - expect(findOneSpy).toHaveBeenCalled(); - expect(findOneSpy).toHaveBeenCalledTimes(1); - }); - - test('Returns 500 if error occurred while fetching time-off request with an id', async () => { - const { getTimeOffRequestbyId } = makeSut(); - - const error = new Error('Some error occurred.'); - const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockRejectedValueOnce(error); - - const response = await getTimeOffRequestbyId(mockReq, mockRes); - await flushPromises(); - assertResMock(500, error, response, mockRes); - expect(findOneSpy).toHaveBeenCalled(); - expect(findOneSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('deleteTimeOffRequestById function', () => { - test('Returns 403 if user is not authorized', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - // Creating a deep copy of mockReq - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'volunteer'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = []; - mockReqCopy.params.id = '123'; - - const error = 'You are not authorized to set time off requests.'; - - const mockData = null; - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockResolvedValueOnce(mockData); - - hasPermission.mockImplementation(async () => false); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(403, error, response, mockRes); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalled(); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test('Returns 404 if no timeOffRequest exists with the particular Id', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - // Creating a deep copy of mockReq - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'volunteer'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = []; - mockReqCopy.params.id = '123'; - - const error = 'You are not authorized to set time off requests.'; - - const mockData = null; - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockResolvedValueOnce(mockData); - - hasPermission.mockImplementation(async () => false); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(403, error, response, mockRes); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalled(); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test('Returns 500 if an error occurs at TimeOffRequest.findById()', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; - mockReqCopy.params.id = '123'; - - const errorMessage = 'Internal Server Error'; - const error = new Error(errorMessage); - - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockImplementationOnce(() => { - throw error; - }); - - hasPermission.mockImplementation(async () => true); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(500, error, response, mockRes); - - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - }); - - test('Returns 500 if an error occurs while TimeOffRequest.findByIdAndDelete()', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; - mockReqCopy.params.id = '123'; - - const errorMessage = 'Internal Server Error'; - const error = new Error(errorMessage); - - const mockData = { - requestFor: 'sd9028_sdas83ink84haso1', - reason: 'Family Gathering.', - startingDate: new Date(2024, 5, 1), - endingDate: new Date(2024, 5, 13), - duration: 2, - }; - - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockImplementationOnce(() => mockData); - const findByIdAndDeleteSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndDelete') - .mockImplementationOnce(() => { - throw error; - }); - - hasPermission.mockImplementation(async () => error); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(500, error, response, mockRes); - - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(findByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(findByIdAndDeleteSpy).toHaveBeenCalledTimes(1); - }); - - test('Returns 200 on successfully deleting the TimeOffRequest; should not call emailSender as `deleteOwnRequest` is false', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; - mockReqCopy.params.id = '123'; - - const mockData = { - requestFor: 'sd9028_sdas83ink84haso1', - reason: 'Family Gathering.', - startingDate: new Date(2024, 5, 1), - endingDate: new Date(2024, 5, 13), - duration: 2, - }; - - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockImplementationOnce(() => mockData); - const findByIdAndDeleteSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndDelete') - .mockImplementationOnce(() => mockData); - - hasPermission.mockImplementation(async () => true); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(200, mockData, response, mockRes); - - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(findByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(findByIdAndDeleteSpy).toHaveBeenCalledTimes(1); - - expect(emailSender).toHaveBeenCalledTimes(0); - }); - - test('Returns 200 on successfully deleting the TimeOffRequest; notifyUser calls emailSender once and notifyAdmins does not calls emailSender', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; - mockReqCopy.body.requestor.requestorId = 'sd9028_sdas83ink84haso1'; - mockReqCopy.params.id = '123'; - - const mockData = { - requestFor: 'sd9028_sdas83ink84haso1', - reason: 'Family Gathering.', - startingDate: new Date(2024, 5, 1), - endingDate: new Date(2024, 5, 13), - duration: 2, - }; - - const mockedUserData = { - firstName: 'testUserFirstName', - lastName: 'testUserLastName', - email: 'testUser@testing.com', - }; - - const mockedOwnerAccountEmails = [ - // No owner accounts hence NotifyAdmins sends 0 emails - ]; - - const mockedUserTeams = [ - { - // object represents a team 1 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - { - // object represents a team 2 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - ]; - - const mockedUserProfiles = [ - { role: 'Volunteer', email: 'abc_123' }, - { role: 'Tester', email: 'def_456' }, - { role: 'Developer', email: 'ghi_789' }, - { role: 'Volunteer', email: 'jkl_000' }, - { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, - ]; - - const userProfileFindByIdSpy = jest - .spyOn(UserProfile, 'findById') - .mockResolvedValue(mockedUserData); - - const chaining = { - select: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), - }; - - const userEmails = getAdminEmailIds(mockedUserProfiles); - - const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { - if ('role' in query && query.role === 'Owner') { - return chaining; - } - if ('_id' in query && '$in' in query._id) { - // Mocking the query for _id - return Promise.resolve(mockedUserProfiles); - } - }); - - const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); - - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockResolvedValue(mockData); - - const timeOffRequestFindByIdAndDeleteSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndDelete') - .mockResolvedValue(mockData); - - hasPermission.mockImplementation(async () => true); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(200, mockData, response, mockRes); - - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledTimes(1); - - expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); - - expect(userProfileFindSpy).toHaveBeenCalledTimes(2); - - expect(teamFindSpy).toHaveBeenCalledTimes(1); - expect(teamFindSpy).toHaveBeenCalledWith({ 'members.userId': mockData.requestFor }); - - expect(emailSender).toHaveBeenCalledTimes( - 1 + mockedOwnerAccountEmails.length + userEmails.length, - ); // just once by notifyUser & notifyAdmins not called - }); - - test('Returns 200 on successfully deleting the TimeOffRequest; notifyUser calls emailSender once and notifyAdmins calls emailSender 5 times', async () => { - const { deleteTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; - mockReqCopy.body.requestor.requestorId = 'sd9028_sdas83ink84haso1'; - mockReqCopy.params.id = '123'; - - const mockData = { - requestFor: 'sd9028_sdas83ink84haso1', - reason: 'Family Gathering.', - startingDate: new Date(2024, 5, 1), - endingDate: new Date(2024, 5, 13), - duration: 2, - }; - - const mockedUserData = { - firstName: 'testUserFirstName', - lastName: 'testUserLastName', - email: 'testUser@testing.com', - }; - - const mockedOwnerAccountEmails = [ - // No owner accounts hence NotifyAdmins sends 2 emails - { email: 'temp1@gmail.com' }, - { email: 'temp2@gmail.com' }, - ]; - - const mockedUserTeams = [ - { - // object represents a team 1 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - { - // object represents a team 2 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - ]; - - const mockedUserProfiles = [ - { role: 'Manager', email: 'abc_123' }, - { role: 'Tester', email: 'def_456' }, - { role: 'Developer', email: 'ghi_789' }, - { role: 'Administrator', email: 'jkl_000' }, - { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, - ]; - - const userProfileFindByIdSpy = jest - .spyOn(UserProfile, 'findById') - .mockResolvedValue(mockedUserData); - - const chaining = { - select: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), - }; - - const userEmails = getAdminEmailIds(mockedUserProfiles); - - const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { - if ('role' in query && query.role === 'Owner') { - return chaining; - } - if ('_id' in query && '$in' in query._id) { - // Mocking the query for _id - return Promise.resolve(mockedUserProfiles); - } - }); - - const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); - - const timeOffRequestFindByIdSpy = jest - .spyOn(TimeOffRequest, 'findById') - .mockResolvedValue(mockData); - - const timeOffRequestFindByIdAndDeleteSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndDelete') - .mockResolvedValue(mockData); - - hasPermission.mockImplementation(async () => true); - - const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(200, mockData, response, mockRes); - - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); - expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledTimes(1); - - expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); - - expect(userProfileFindSpy).toHaveBeenCalledTimes(2); - - expect(teamFindSpy).toHaveBeenCalledTimes(1); - expect(teamFindSpy).toHaveBeenCalledWith({ 'members.userId': mockData.requestFor }); - - expect(emailSender).toHaveBeenCalledTimes( - 1 + mockedOwnerAccountEmails.length + userEmails.length, - ); // addition of 1 represents emailSender function call by notifyUser Function - }); - }); - - describe('updateTimeOffRequestById function', () => { - test('Returns 403 if user is not authorized', async () => { - const { updateTimeOffRequestById } = makeSut(); - - // Creating a deep copy of mockReq - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'volunteer'; - mockReqCopy.body.requestor.permissions.frontPermissions = []; - mockReqCopy.body.requestor.permissions.backPermissions = []; - mockReqCopy.params.id = '123'; - - const error = 'You are not authorized to set time off requests.'; - - hasPermission.mockImplementation(async () => false); - - const response = await updateTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(403, error, response, mockRes); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test.each` - duration | startingDate | reason | requestId | expectedMessage - ${'1 week'} | ${new Date('2024-06-8')} | ${'Sick'} | ${null} | ${'bad request'} - ${null} | ${new Date('2024-06-8')} | ${'Injury'} | ${'user123'} | ${'bad request'} - ${'5 week'} | ${null} | ${'Wedding'} | ${'user123'} | ${'bad request'} - ${'7 week'} | ${new Date('2024-06-8')} | ${null} | ${'user123'} | ${'bad request'} - `( - `returns 400 when duration is $duration, startingDate is $startingDate, reason is $reason, and requestId is $requestId`, - async ({ duration, startingDate, reason, requestId, expectedMessage }) => { - const { updateTimeOffRequestById } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.body.requestor.role = 'Administrator'; - mockReqCopy.body.requestor.requestorId = 'user123'; - mockReqCopy.params.id = requestId; - - mockReqCopy.body.duration = duration; - mockReqCopy.body.reason = reason; - mockReqCopy.body.startingDate = startingDate; - mockReqCopy.body.requestId = requestId; - - hasPermission.mockImplementation(async () => true); - - const response = await updateTimeOffRequestById(mockReqCopy, mockRes); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - assertResMock(400, expectedMessage, response, mockRes); - }, - ); - - test('Returns 404 if no timeOffRequest is found', async () => { - const { updateTimeOffRequestById } = makeSut(); - - // Creating a deep copy of mockReq - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.params.id = '123'; - - mockReqCopy.body.requestor = { - ...mockReqCopy.body.requestor, // Preserving existing properties - role: 'Owner', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - }; - - const timeOffDuration = 5; - const timeOffStartingDate = new Date(2024, 5, 12); - const timeOffReason = 'Testing a leave request'; - - moment.tz.setDefault('America/Los_Angeles'); - - const startDate = moment(timeOffStartingDate); - const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); - - const mockUpdateData = { - reason: timeOffReason, - startingDate: startDate.toDate(), - endingDate: endDate.toDate(), - duration: timeOffDuration, - }; - - mockReqCopy.body = { - ...mockReqCopy.body, - duration: timeOffDuration, - startingDate: timeOffStartingDate, - reason: timeOffReason, - }; - - const error = 'Time off request not found'; - - hasPermission.mockImplementation(async () => true); - const timeOffRequestFindByIdAndUpdateSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndUpdate') - .mockImplementationOnce(() => Promise.resolve(null)); - - const response = await updateTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(404, error, response, mockRes); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( - mockReqCopy.params.id, - mockUpdateData, - { - new: true, - }, - ); - }); - - test('Returns 200 on successful update operation', async () => { - const { updateTimeOffRequestById } = makeSut(); - - // Creating a deep copy of mockReq - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.params.id = '123'; - - mockReqCopy.body.requestor = { - ...mockReqCopy.body.requestor, - role: 'Owner', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - }; - - const timeOffDuration = 5; - const timeOffStartingDate = new Date(2024, 5, 12); - const timeOffReason = 'Testing a leave request'; - - moment.tz.setDefault('America/Los_Angeles'); - const startDate = moment(timeOffStartingDate); - const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); - - const mockUpdateData = { - reason: timeOffReason, - startingDate: startDate.toDate(), - endingDate: endDate.toDate(), - duration: timeOffDuration, - }; - - mockReqCopy.body = { - ...mockReqCopy.body, - duration: timeOffDuration, - startingDate: timeOffStartingDate, - reason: timeOffReason, - }; - - hasPermission.mockImplementation(async () => true); - const timeOffRequestFindByIdAndUpdateSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndUpdate') - .mockImplementationOnce(() => Promise.resolve(mockUpdateData)); - - const response = await updateTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(200, mockUpdateData, response, mockRes); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( - mockReqCopy.params.id, - mockUpdateData, - { - new: true, - }, - ); - }); - - test('Returns 500 if error occurs with findByIdAndUpdate ', async () => { - const { updateTimeOffRequestById } = makeSut(); - - // Creating a deep copy of the `mockReq` - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - mockReqCopy.params.id = '123'; - - mockReqCopy.body.requestor = { - ...mockReqCopy.body.requestor, - role: 'Owner', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - }; - - const timeOffDuration = 5; - const timeOffStartingDate = new Date(2024, 5, 12); - const timeOffReason = 'Testing a leave request'; - - moment.tz.setDefault('America/Los_Angeles'); - const startDate = moment(timeOffStartingDate); - const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); - - const mockUpdateData = { - reason: timeOffReason, - startingDate: startDate.toDate(), - endingDate: endDate.toDate(), - duration: timeOffDuration, - }; - - mockReqCopy.body = { - ...mockReqCopy.body, - duration: timeOffDuration, - startingDate: timeOffStartingDate, - reason: timeOffReason, - }; - - const error = new Error('Some error occcurred during operation findByIdAndUpdate()'); - - hasPermission.mockImplementation(async () => true); - const timeOffRequestFindByIdAndUpdateSpy = jest - .spyOn(TimeOffRequest, 'findByIdAndUpdate') - .mockRejectedValueOnce(error); - - const response = await updateTimeOffRequestById(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(500, error, response, mockRes); - - expect(hasPermission).toHaveBeenCalledWith( - mockReqCopy.body.requestor, - 'manageTimeOffRequests', - ); - expect(hasPermission).toHaveBeenCalledTimes(1); - - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); - expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( - mockReqCopy.params.id, - mockUpdateData, - { - new: true, - }, - ); - }); - }); - - describe('setTimeOffRequest function', () => { - test('Returns 403 if the user is not authorised', async () => { - const { setTimeOffRequest } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Volunteer', - requestorId: 'testUser123', - }, - requestFor: 'testUser456', - }; - - hasPermission.mockImplementation(async () => Promise.resolve(false)); - - const error = 'You are not authorized to set time off requests.'; - - const response = await setTimeOffRequest(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(403, error, response, mockRes); - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - }); - - test('Returns 201 if the time-off request is set successfully; emailSender is not called as setOwnRequested is False', async () => { - // emailSender is not called as setOwnRequested is False - const { setTimeOffRequest } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Administrator', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - requestorId: 'testUser123', - }, - requestFor: 'testUser456', - duration: 1, - startingDate: new Date(2024, 5, 15), - reason: 'Test set time off', - }; - - const mockedResponseDocument = { - requestFor: mockReqCopy.body.requestFor, - duration: mockReqCopy.body.duration, - startingDate: mockReqCopy.body.startDate, - reason: mockReqCopy.body.reason, - endingDate: new Date(2024, 5, 21), - }; - - hasPermission.mockImplementation(async () => Promise.resolve(true)); - const mongooseObjectIdSpy = jest - .spyOn(mongoose.Types, 'ObjectId') - .mockImplementationOnce(() => mockReqCopy.body.requestFor); - const timeOffRequestSaveSpy = jest - .spyOn(TimeOffRequest.prototype, 'save') - .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); - - const response = await setTimeOffRequest(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(201, mockedResponseDocument, response, mockRes); - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - - expect(mongooseObjectIdSpy).toBeCalled(); - expect(mongooseObjectIdSpy).toBeCalledTimes(1); - expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); - - expect(timeOffRequestSaveSpy).toBeCalled(); - expect(timeOffRequestSaveSpy).toBeCalledTimes(1); - - expect(emailSender).toHaveBeenCalledTimes(0); - }); - - test('Returns 201 if the time-off request is set successfully; emailSender is not called as savedRequest is null', async () => { - // emailSender is not called as savedRequest is null - const { setTimeOffRequest } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Administrator', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - requestorId: 'testUser123', - }, - requestFor: 'testUser123', - duration: 1, - startingDate: new Date(2024, 5, 15), - reason: 'Test set time off', - }; - - const mockedResponseDocument = null; - - hasPermission.mockImplementation(async () => Promise.resolve(true)); - const mongooseObjectIdSpy = jest - .spyOn(mongoose.Types, 'ObjectId') - .mockImplementationOnce(() => mockReqCopy.body.requestFor); - const timeOffRequestSaveSpy = jest - .spyOn(TimeOffRequest.prototype, 'save') - .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); - - const response = await setTimeOffRequest(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(201, mockedResponseDocument, response, mockRes); - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - - expect(mongooseObjectIdSpy).toBeCalled(); - expect(mongooseObjectIdSpy).toBeCalledTimes(1); - expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); - - expect(timeOffRequestSaveSpy).toBeCalled(); - expect(timeOffRequestSaveSpy).toBeCalledTimes(1); - - expect(emailSender).toHaveBeenCalledTimes(0); - }); - - test('Returns 201 if the time-off request is set successfully; emailSender is called', async () => { - // emailSender is called as savedRequest is not null and setOwnRequested is True - const { setTimeOffRequest } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Administrator', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - requestorId: 'testUser123', - }, - requestFor: 'testUser123', - duration: 1, - startingDate: new Date(2024, 5, 15), - reason: 'Test set time off', - }; - - mockReqCopy.params.id = 'mockId'; - - const mockedResponseDocument = { - requestFor: mockReqCopy.body.requestFor, - duration: mockReqCopy.body.duration, - startingDate: mockReqCopy.body.startDate, - reason: mockReqCopy.body.reason, - endingDate: new Date(2024, 5, 21), - }; - - const mockedOwnerAccountEmails = [ - // No owner accounts hence NotifyAdmins sends 0 emails - ]; - - const mockedUserTeams = [ - { - // object represents a team 1 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - { - // object represents a team 2 - members: [ - // array represents team members - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, - { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, - ], - }, - ]; - - const mockedUserProfiles = [ - { role: 'Volunteer', email: 'abc_123' }, - { role: 'Tester', email: 'def_456' }, - { role: 'Developer', email: 'ghi_789' }, - { role: 'Volunteer', email: 'jkl_000' }, - { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, - ]; - - const mockedUserData = { - firstName: 'testUserFirstName', - lastName: 'testUserLastName', - email: 'testUser@testing.com', - }; - - const userProfileFindByIdSpy = jest - .spyOn(UserProfile, 'findById') - .mockResolvedValue(mockedUserData); - - const chaining = { - select: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), - }; - - const userEmails = getAdminEmailIds(mockedUserProfiles); - - const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { - if ('role' in query && query.role === 'Owner') { - return chaining; - } - if ('_id' in query && '$in' in query._id) { - // Mocking the query for _id - return Promise.resolve(mockedUserProfiles); - } - }); - - const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); - - hasPermission.mockImplementation(async () => Promise.resolve(true)); - const mongooseObjectIdSpy = jest - .spyOn(mongoose.Types, 'ObjectId') - .mockImplementationOnce(() => mockReqCopy.body.requestFor); - const timeOffRequestSaveSpy = jest - .spyOn(TimeOffRequest.prototype, 'save') - .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); - - const response = await setTimeOffRequest(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(201, mockedResponseDocument, response, mockRes); - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - - expect(mongooseObjectIdSpy).toBeCalled(); - expect(mongooseObjectIdSpy).toBeCalledTimes(1); - expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); - - expect(timeOffRequestSaveSpy).toBeCalled(); - expect(timeOffRequestSaveSpy).toBeCalledTimes(1); - - expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); - - expect(userProfileFindSpy).toHaveBeenCalledTimes(2); - - expect(teamFindSpy).toHaveBeenCalledTimes(1); - expect(teamFindSpy).toHaveBeenCalledWith({ - 'members.userId': mockedResponseDocument.requestFor, - }); - - expect(emailSender).toHaveBeenCalledTimes( - 1 + mockedOwnerAccountEmails.length + userEmails.length, - ); - }); - - test.each` - duration | startingDate | reason | requestFor | expectedMessage - ${null} | ${new Date('2024-06-08')} | ${'Injury'} | ${'user123'} | ${'bad request'} - ${'5 week'} | ${null} | ${'Wedding'} | ${'user123'} | ${'bad request'} - ${'7 week'} | ${new Date('2024-06-08')} | ${null} | ${'user123'} | ${'bad request'} - ${'1 week'} | ${new Date('2024-06-08')} | ${'Sick'} | ${null} | ${'bad request'} - `( - `Return 400 if request body is missing any one of the following $requestFor, $reason, $duration, or $startingDate`, - async ({ duration, startingDate, reason, requestFor, expectedMessage }) => { - const { setTimeOffRequest } = makeSut(); - - hasPermission.mockImplementationOnce(async () => Promise.resolve(true)); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Administrator', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - requestorId: 'testUser123', - }, - requestFor, - duration, - startingDate, - reason, - }; - - const error = expectedMessage; - const response = await setTimeOffRequest(mockReqCopy, mockRes); - - assertResMock(400, error, response, mockRes); - - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - }, - ); - - test('Returns 500 if error occurs while saving time-off request.', async () => { - const { setTimeOffRequest } = makeSut(); - - const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); - - mockReqCopy.body = { - ...mockReqCopy.body, - requestor: { - role: 'Administrator', - permissions: { - frontPermissions: [], - backPermissions: [], - }, - requestorId: 'testUser123', - }, - requestFor: 'testUser456', - duration: 1, - startingDate: new Date(2024, 5, 15), - reason: 'Test set time off', - }; - - const error = 'Error saving the request.'; - - hasPermission.mockImplementation(async () => Promise.resolve(true)); - const mongooseObjectIdSpy = jest - .spyOn(mongoose.Types, 'ObjectId') - .mockImplementationOnce(() => mockReqCopy.body.requestFor); - const timeOffRequestSaveSpy = jest - .spyOn(TimeOffRequest.prototype, 'save') - .mockRejectedValueOnce(error); - - const response = await setTimeOffRequest(mockReqCopy, mockRes); - await flushPromises(); - - assertResMock(500, error, response, mockRes); - - expect(hasPermission).toBeCalled(); - expect(hasPermission).toBeCalledTimes(1); - expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); - - expect(mongooseObjectIdSpy).toBeCalled(); - expect(mongooseObjectIdSpy).toBeCalledTimes(1); - expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); - - expect(timeOffRequestSaveSpy).toBeCalled(); - expect(timeOffRequestSaveSpy).toBeCalledTimes(1); - }); - }); -}); diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index f351c6e77..3bb268143 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,7 +1,11 @@ const Team = require('../models/team'); const Project = require('../models/project'); +const cacheClosure = require('../utilities/nodeCache'); +const { getAllTeamCodeHelper } = require("./userProfileController"); const titlecontroller = function (Title) { + const cache = cacheClosure(); + const getAllTitles = function (req, res) { Title.find({}) .then((results) => res.status(200).send(results)) @@ -97,11 +101,15 @@ const titlecontroller = function (Title) { res.status(500).send(error); }); }; - + // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. async function checkTeamCodeExists(teamCode) { try { - const team = await Team.findOne({ teamCode }).exec(); - return !!team; + if (cache.getCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes.includes(teamCode); + } + const teamCodes = await getAllTeamCodeHelper(); + return teamCodes.includes(teamCode); } catch (error) { console.error('Error checking if team code exists:', error); throw error; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index a61cf3c43..efae2c139 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -4,6 +4,7 @@ const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); // eslint-disable-next-line import/no-extraneous-dependencies const fetch = require('node-fetch'); + const moment_ = require('moment'); const jwt = require('jsonwebtoken'); const userHelper = require('../helpers/userHelper')(); @@ -13,9 +14,9 @@ const Badge = require('../models/badge'); const yearMonthDayDateValidator = require('../utilities/yearMonthDayDateValidator'); const cacheClosure = require('../utilities/nodeCache'); const followUp = require('../models/followUp'); -const userService = require('../services/userService'); + // const { authorizedUserSara, authorizedUserJae } = process.env; -const authorizedUserSara = `nathaliaowner@gmail.com`; // To test this code please include your email here +const authorizedUserSara = `sucheta_mu@test.com`; // To test this code please include your email here const authorizedUserJae = `jae@onecommunityglobal.org`; const { hasPermission, canRequestorUpdateUser } = require('../utilities/permissions'); @@ -23,10 +24,7 @@ const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); const emailSender = require('../utilities/emailSender'); -const objectUtils = require('../utilities/objectUtils'); - const config = require('../config'); -const { PROTECTED_EMAIL_ACCOUNT } = require('../utilities/constants'); async function ValidatePassword(req, res) { const { userId } = req.params; @@ -76,87 +74,7 @@ async function ValidatePassword(req, res) { } } -const sendEmailUponProtectedAccountUpdate = ( - requestorEmail, - requestorFullName, - targetEmail, - action, - logId, -) => { - const updatedDate = moment_().format('MMM-DD-YY'); - const subject = 'One Community: Protected Account Has Been Updated'; - const emailBody = `

Hi Admin!

- -

Protected Account ${targetEmail} is updated by ${requestorEmail}

- -

Here are the details for the new ${targetEmail} account:

-
    -
  • Updated Date: ${updatedDate}
  • -
  • Action: ${action}
  • -
- -

Who updated this new account?

- - -

If you have any questions or notice any issues, - please investigate further by searching log transaction ID ${logId} in the Sentry .

- -

Thank you for your attention to this matter.

- -

Sincerely,

-

The HGN (and One Community)

`; - emailSender(targetEmail, subject, emailBody, null, null); -}; - -const auditIfProtectedAccountUpdated = async ( - requestorId, - updatedRecordEmail, - originalRecord, - updatedRecord, - updateDiffPaths, - actionPerformed, -) => { - if (PROTECTED_EMAIL_ACCOUNT.includes(updatedRecordEmail)) { - const requestorProfile = await userService.getUserFullNameAndEmailById(requestorId); - const requestorFullName = requestorProfile - ? requestorProfile.firstName.concat(' ', requestorProfile.lastName) - : 'N/A'; - // remove sensitive data from the original and updated records - let extraData = null; - const updateObject = updatedRecord.toObject(); - if (updateDiffPaths) { - const { originalObj, updatedObj } = objectUtils.returnObjectDifference( - originalRecord, - updateObject, - updateDiffPaths, - ); - const originalObjectString = originalRecord ? JSON.stringify(originalObj) : null; - const updatedObjectString = updatedRecord ? JSON.stringify(updatedObj) : null; - extraData = { - originalObjectString, - updatedObjectString, - }; - } - const logId = logger.logInfo( - `Protected email account updated. Target: ${updatedRecordEmail} - Requestor: ${requestorProfile ? requestorFullName : requestorId}`, - extraData, - ); - - sendEmailUponProtectedAccountUpdate( - requestorProfile?.email, - requestorFullName, - updatedRecordEmail, - actionPerformed, - logId, - ); - } -}; - -const userProfileController = function (UserProfile, Project) { +const userProfileController = function (UserProfile) { const cache = cacheClosure(); const forbidden = function (res, message) { @@ -166,7 +84,6 @@ const userProfileController = function (UserProfile, Project) { const checkPermission = async function (req, permission) { return helper.hasPermission(req.body.requestor, permission); }; - const getUserProfiles = async function (req, res) { if (!(await checkPermission(req, 'getUserProfiles'))) { forbidden(res, 'You are not authorized to view all users'); @@ -175,6 +92,7 @@ const userProfileController = function (UserProfile, Project) { await UserProfile.find( {}, + '_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate startDate createdDate endDate', ) .sort({ @@ -357,26 +275,26 @@ const userProfileController = function (UserProfile, Project) { const subject = `${process.env.dbName !== 'hgnData_dev' ? '*Main Site* -' : ''}New ${up.role} Role Created`; const emailBody = `

Hi Admin!

- +

New Account Details

This email is to inform you that ${up.firstName} ${up.lastName} has been created as a new ${up.role} account on the Highest Good Network application.

- +

Here are the details for the new ${up.role} account:

  • Name: ${up.firstName} ${up.lastName}
  • Email: ${up.email}
- +

Who created this new account?

- +

If you have any questions or notice any issues, please investigate further.

- +

Thank you for your attention to this matter.

- +

Sincerely,

The HGN A.I. (and One Community)

`; @@ -413,20 +331,13 @@ const userProfileController = function (UserProfile, Project) { const putUserProfile = async function (req, res) { const userid = req.params.userId; - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userid, - ); - const isRequestorAuthorized = !!( - canEditProtectedAccount && + canRequestorUpdateUser(req.body.requestor.requestorId, userid) && ((await hasPermission(req.body.requestor, 'putUserProfile')) || req.body.requestor.requestorId === userid) ); - const canManageAdminLinks = await hasPermission(req.body.requestor, 'manageAdminLinks'); - - if (!isRequestorAuthorized && !canManageAdminLinks) { + if (!isRequestorAuthorized) { res.status(403).send('You are not authorized to update this user'); return; } @@ -445,13 +356,6 @@ const userProfileController = function (UserProfile, Project) { res.status(404).send('No valid records found'); return; } - - // To keep a copy of the original record if we edit the protected account - let originalRecord = {}; - if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { - originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(record); - // console.log('originalRecord', originalRecord); - } // validate userprofile pic if (req.body.profilePic) { @@ -482,10 +386,12 @@ const userProfileController = function (UserProfile, Project) { 'profilePic', 'firstName', 'lastName', + 'jobTitle', 'phoneNumber', 'bio', 'personalLinks', 'location', + 'profilePic', 'privacySettings', 'weeklySummaries', 'weeklySummariesCount', @@ -495,8 +401,8 @@ const userProfileController = function (UserProfile, Project) { 'totalTangibleHrs', 'totalIntangibleHrs', 'isFirstTimelog', - 'teamCode', 'isVisible', + 'isRehireable', 'bioPosted', ]; @@ -506,6 +412,16 @@ const userProfileController = function (UserProfile, Project) { } }); + // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), + // we need to remove the cache when team code is updated in case of new team code generation + if (req.body.teamCode) { + // remove teamCode cache when new team assigned + if (req.body.teamCode !== record.teamCode) { + cache.removeCache('teamCodes'); + } + record.teamCode = req.body.teamCode; + } + record.lastModifiedDate = Date.now(); // find userData in cache @@ -518,29 +434,19 @@ const userProfileController = function (UserProfile, Project) { userIdx = allUserData.findIndex((users) => users._id === userid); userData = allUserData[userIdx]; } - if (await hasPermission(req.body.requestor, 'updateSummaryRequirements')) { - const summaryFields = ['weeklySummaryNotReq', 'weeklySummaryOption']; - summaryFields.forEach((fieldName) => { - if (req.body[fieldName] !== undefined) { - record[fieldName] = req.body[fieldName]; - } - }); - } - - if (req.body.adminLinks !== undefined && canManageAdminLinks) { - record.adminLinks = req.body.adminLinks; - } - if (await hasPermission(req.body.requestor, 'putUserProfileImportantInfo')) { const importantFields = [ - 'email', 'role', 'isRehireable', 'isActive', + 'adminLinks', + 'isActive', 'weeklySummaries', 'weeklySummariesCount', 'mediaUrl', 'collaborationPreference', + 'weeklySummaryNotReq', + 'weeklySummaryOption', 'categoryTangibleHrs', 'totalTangibleHrs', 'timeEntryEditHistory', @@ -570,39 +476,7 @@ const userProfileController = function (UserProfile, Project) { } if (req.body.projects !== undefined) { - const newProjects = req.body.projects.map((project) => project._id.toString()); - - // check if the projects have changed - const projectsChanged = - !record.projects.every((id) => newProjects.includes(id.toString())) || - !newProjects.every((id) => record.projects.map((p) => p.toString()).includes(id)); - - if (projectsChanged) { - // store the old projects for comparison - const oldProjects = record.projects.map((id) => id.toString()); - - // update the projects - record.projects = newProjects.map((id) => mongoose.Types.ObjectId(id)); - - const addedProjects = newProjects.filter((id) => !oldProjects.includes(id)); - const removedProjects = oldProjects.filter((id) => !newProjects.includes(id)); - - const changedProjectIds = [...addedProjects, ...removedProjects].map((id) => - mongoose.Types.ObjectId(id), - ); - - if (changedProjectIds.length > 0) { - const now = new Date(); - Project.updateMany( - { _id: { $in: changedProjectIds } }, - { $set: { membersModifiedDatetime: now } }, - ) - .exec() - .catch((error) => { - console.error('Error updating project membersModifiedDatetime:', error); - }); - } - } + record.projects = Array.from(new Set(req.body.projects)); } if (req.body.email !== undefined) { @@ -681,10 +555,7 @@ const userProfileController = function (UserProfile, Project) { ) { record.infringements = req.body.infringements; } - let updatedDiff = null; - if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { - updatedDiff = record.modifiedPaths(); - } + record .save() .then((results) => { @@ -697,7 +568,6 @@ const userProfileController = function (UserProfile, Project) { results.role, results.startDate, results.jobTitle[0], - results.weeklycommittedHours, ); res.status(200).json({ _id: record._id, @@ -708,15 +578,6 @@ const userProfileController = function (UserProfile, Project) { allUserData.splice(userIdx, 1, userData); cache.setCache('allusers', JSON.stringify(allUserData)); } - // Log the update of a protected email account - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - originalRecord.email, - originalRecord, - record, - updatedDiff, - 'update', - ); }) .catch((error) => res.status(400).send(error)); }); @@ -724,10 +585,6 @@ const userProfileController = function (UserProfile, Project) { const deleteUserProfile = async function (req, res) { const { option, userId } = req.body; - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userId, - ); if (!(await hasPermission(req.body.requestor, 'deleteUserProfile'))) { res.status(403).send('You are not authorized to delete users'); return; @@ -757,18 +614,6 @@ const userProfileController = function (UserProfile, Project) { const user = await UserProfile.findById(userId); - // Check if the user is protected and if the requestor has permission to delete protected accounts - if (PROTECTED_EMAIL_ACCOUNT.includes(user.email) && !canEditProtectedAccount) { - res.status(403).send({ - error: 'Only authorized users can delete protected accounts', - }); - // - logger.logInfo( - `Unauthorized attempt to delete a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${user.email}`, - ); - return; - } - if (!user) { res.status(400).send({ error: 'Invalid user', @@ -813,19 +658,12 @@ const userProfileController = function (UserProfile, Project) { allUserData.splice(userIdx, 1); cache.setCache('allusers', JSON.stringify(allUserData)); } - const originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(user); + try { await UserProfile.deleteOne({ _id: userId }); // delete followUp for deleted user await followUp.findOneAndDelete({ userId }); res.status(200).send({ message: 'Executed Successfully' }); - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - originalRecord.email, - originalRecord, - null, - 'delete', - ); } catch (err) { res.status(500).send(err); } @@ -899,23 +737,10 @@ const userProfileController = function (UserProfile, Project) { .catch((error) => res.status(404).send(error)); }; - const updateOneProperty = async function (req, res) { + const updateOneProperty = function (req, res) { const { userId } = req.params; const { key, value } = req.body; - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userId, - ); - - if (!canEditProtectedAccount) { - logger.logInfo( - `Unauthorized attempt to update a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, - ); - res.status(403).send('You are not authorized to update this user'); - return; - } - if (key === 'teamCode') { const canEditTeamCode = req.body.requestor.role === 'Owner' || @@ -936,29 +761,14 @@ const userProfileController = function (UserProfile, Project) { return UserProfile.findById(userId) .then((user) => { - let originalRecord = null; - if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { - originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(user); - } user.set({ [key]: value, }); - let updatedDiff = null; - if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { - updatedDiff = user.modifiedPaths(); - } + return user .save() .then(() => { res.status(200).send({ message: 'updated property' }); - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - originalRecord.email, - originalRecord, - user, - updatedDiff, - 'update', - ); }) .catch((error) => res.status(500).send(error)); }) @@ -981,19 +791,6 @@ const userProfileController = function (UserProfile, Project) { }); } // Check if the requestor has the permission to update passwords. - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userId, - ); - - if (!canEditProtectedAccount) { - logger.logInfo( - `Unauthorized attempt to update a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, - ); - res.status(403).send('You are not authorized to update this user'); - return; - } - const hasUpdatePasswordPermission = await hasPermission(requestor, 'updatePassword'); // if they're updating someone else's password, they need the 'updatePassword' permission. @@ -1034,21 +831,7 @@ const userProfileController = function (UserProfile, Project) { }); return user .save() - .then(() => { - if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { - logger.logInfo( - `Protected email account password updated. Requestor: ${req.body.requestor.requestorId}, Target: ${user.email}`, - ); - } - res.status(200).send({ message: 'updated password' }); - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - user.email, - null, - null, - 'PasswordUpdate', - ); - }) + .then(() => res.status(200).send({ message: 'updated password' })) .catch((error) => res.status(500).send(error)); }) .catch((error) => res.status(500).send(error)); @@ -1139,46 +922,12 @@ const userProfileController = function (UserProfile, Project) { }); return; } - - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userId, - ); - - if ( - !((await hasPermission(req.body.requestor, 'changeUserStatus')) && canEditProtectedAccount) - ) { - if (PROTECTED_EMAIL_ACCOUNT.includes(req.body.requestor.email)) { - logger.logInfo( - `Unauthorized attempt to change protected user status. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, - ); - } + if (!(await hasPermission(req.body.requestor, 'changeUserStatus'))) { res.status(403).send('You are not authorized to change user status'); return; } cache.removeCache(`user-${userId}`); - const emailReceivers = await UserProfile.find( - { isActive: true, role: { $in: ['Owner'] } }, - '_id isActive role email', - ); - - const recipients = emailReceivers.map((receiver) => receiver.email); - - try { - const findUser = await UserProfile.findById(userId, 'teams'); - findUser.teams.map(async (teamId) => { - const managementEmails = await userHelper.getTeamManagementEmail(teamId); - if (Array.isArray(managementEmails) && managementEmails.length > 0) { - managementEmails.forEach((management) => { - recipients.push(management.email); - }); - } - }); - } catch (err) { - logger.logException(err, 'Unexpected error in finding menagement team'); - } - - UserProfile.findById(userId, 'isActive email firstName lastName') + UserProfile.findById(userId, 'isActive') .then((user) => { user.set({ isActive: status, @@ -1201,20 +950,6 @@ const userProfileController = function (UserProfile, Project) { allUserData.splice(userIdx, 1, userData); cache.setCache('allusers', JSON.stringify(allUserData)); } - userHelper.sendDeactivateEmailBody( - user.firstName, - user.lastName, - endDate, - user.email, - recipients, - ); - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - user.email, - null, - null, - 'UserStatusUpdate', - ); res.status(200).send({ message: 'status updated', }); @@ -1231,17 +966,11 @@ const userProfileController = function (UserProfile, Project) { const changeUserRehireableStatus = async function (req, res) { const { userId } = req.params; const { isRehireable } = req.body; - const canEditProtectedAccount = await canRequestorUpdateUser( - req.body.requestor.requestorId, - userId, - ); + if (!mongoose.Types.ObjectId.isValid(userId)) { return res.status(400).send({ error: 'Bad Request' }); } - if ( - !(await hasPermission(req.body.requestor, 'changeUserRehireableStatus')) || - !canEditProtectedAccount - ) { + if (!(await hasPermission(req.body.requestor, 'changeUserRehireableStatus'))) { return res.status(403).send('You are not authorized to change rehireable status'); } @@ -1273,13 +1002,6 @@ const userProfileController = function (UserProfile, Project) { if (err) { return res.status(500).send('Error fetching updated user data.'); } - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - verifiedUser.email, - null, - null, - 'UserRehireableStatusUpdate', - ); res.status(200).send({ message: 'Rehireable status updated and verified successfully', isRehireable: verifiedUser.isRehireable, @@ -1336,15 +1058,15 @@ const userProfileController = function (UserProfile, Project) {

Account Details

This email is to inform you that a password reset has been executed for an ${user.role} account:

- + - +

Account that reset the ${user.role}'s password

The password reset was made by:

- +
  • Name: ${requestor.firstName} ${requestor.lastName}
  • Email: ${requestor.email}
  • @@ -1353,7 +1075,7 @@ const userProfileController = function (UserProfile, Project) {

    If you have any questions or need to verify this password reset, please investigate further.

    Thank you for your attention to this matter.

    - +

    Sincerely,

    The HGN A.I. (and One Community)

    `; @@ -1364,13 +1086,6 @@ const userProfileController = function (UserProfile, Project) { res.status(200).send({ message: 'Password Reset', }); - auditIfProtectedAccountUpdated( - req.body.requestor.requestorId, - user.email, - null, - null, - 'UserResetPassword', - ); } catch (error) { res.status(500).send(error); } @@ -1453,11 +1168,15 @@ const userProfileController = function (UserProfile, Project) { const getUserByFullName = (req, res) => { // Sanitize user input and escape special characters const sanitizedFullName = escapeRegExp(req.params.fullName.trim()); + // Create a regular expression to match the sanitized full name, ignoring case const fullNameRegex = new RegExp(sanitizedFullName, 'i'); - + UserProfile.find({ - $or: [{ firstName: { $regex: fullNameRegex } }, { lastName: { $regex: fullNameRegex } }], + $or: [ + { firstName: { $regex: fullNameRegex } }, + { lastName: { $regex: fullNameRegex } }, + ], }) .select('firstName lastName') // eslint-disable-next-line consistent-return @@ -1465,15 +1184,14 @@ const userProfileController = function (UserProfile, Project) { if (users.length === 0) { return res.status(404).send({ error: 'Users Not Found' }); } - res.status(200).send(users); }) .catch((error) => res.status(500).send(error)); }; - // function escapeRegExp(string) { - // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // } + function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } /** * Authorizes user to be able to add Weekly Report Recipients * @@ -1514,56 +1232,33 @@ const userProfileController = function (UserProfile, Project) { } }; - const getProjectsByPerson = async function (req, res) { + const getAllTeamCodeHelper = async function () { try { - const { name } = req.params; - const match = name.trim().split(' '); - const firstName = match[0]; - const lastName = match[match.length - 1]; - - const query = match[1] - ? { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - $and: [ - { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, - { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, - ], - }, - ], - } - : { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - ], - }; + if (cache.hasCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes; + } + const distinctTeamCodes = await UserProfile.distinct('teamCode', { + teamCode: { $ne: null } + }); + cache.setCache('teamCodes', JSON.stringify(distinctTeamCodes)); + return distinctTeamCodes; + } catch (error) { + throw new Error('Encountered an error to get all team codes, please try again!'); + } + } - const userProfile = await UserProfile.find(query); + const getAllTeamCode = async function (req, res) { + try { + const distinctTeamCodes = await getAllTeamCodeHelper(); + return res.status(200).send({ message: 'Found', distinctTeamCodes }); + } catch (error) { + return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); + } + } - if (userProfile) { - const allProjects = userProfile - .map((user) => user.projects) - .filter((projects) => projects.length > 0) - .flat(); - if (allProjects.length === 0) { - return res.status(400).send({ message: 'Projects not found' }); - } - return res.status(200).send({ message: 'Found profile and related projects', allProjects }); - } - } catch (error) { - return res.status(500).send({ massage: 'Encountered an error, please try again!' }); - } - }; return { postUserProfile, @@ -1586,7 +1281,8 @@ const userProfileController = function (UserProfile, Project) { getUserByFullName, changeUserRehireableStatus, authorizeUser, - getProjectsByPerson, + getAllTeamCode, + getAllTeamCodeHelper, }; }; diff --git a/src/controllers/wbsController.js b/src/controllers/wbsController.js index 074cfaf16..2e325b85b 100644 --- a/src/controllers/wbsController.js +++ b/src/controllers/wbsController.js @@ -1,23 +1,20 @@ /* eslint-disable quotes */ /* eslint-disable no-unused-vars */ const mongoose = require('mongoose'); -const { hasPermission } = require('../utilities/permissions'); +const helper = require('../utilities/permissions'); const Project = require('../models/project'); const Task = require('../models/task'); const wbsController = function (WBS) { const getAllWBS = function (req, res) { - WBS.find( - { projectId: { $in: [req.params.projectId] }, isActive: { $ne: false } }, - 'wbsName isActive modifiedDatetime', - ) + WBS.find({ projectId: { $in: [req.params.projectId] } }, 'wbsName isActive modifiedDatetime') .sort({ modifiedDatetime: -1 }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const postWBS = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'postWbs'))) { + if (!(await helper.hasPermission(req.body.requestor, 'postWbs'))) { res.status(403).send({ error: 'You are not authorized to create new projects.' }); return; } @@ -45,13 +42,13 @@ const wbsController = function (WBS) { _wbs .save() - .then((results) => res.status(201).send(results)) - .catch((error) => res.status(500).send({ error })); + .then(results => res.status(201).send(results)) + .catch(error => res.status(500).send({ error })); }; const deleteWBS = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'deleteWbs'))) { - res.status(403).send({ error: 'You are not authorized to delete projects.' }); + if (!(await helper.hasPermission(req.body.requestor, 'deleteWbs'))) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { id } = req.params; @@ -69,12 +66,15 @@ const wbsController = function (WBS) { res.status(400).send(errors); }); }); + // .catch((errors) => { + // res.status(400).send(errors); + // }); }; const getWBS = function (req, res) { - WBS.find({ isActive: { $ne: false } }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(500).send({ error })); + WBS.find() + .then(results => res.status(200).send(results)) + .catch(error => res.status(500).send({ error })); }; const getWBSById = function (req, res) { @@ -83,7 +83,29 @@ const wbsController = function (WBS) { .then((results) => { res.status(200).send(results); }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); + }; + + const getWBSByUserId = async function (req, res) { + const { userId } = req.params; + try { + const result = await Task.aggregate() + .match({ 'resources.userID': mongoose.Types.ObjectId(userId) }) + .project('wbsId -_id') + .group({ _id: '$wbsId' }) + .lookup({ + from: 'wbs', + localField: '_id', + foreignField: '_id', + as: 'wbs', + }) + .unwind('wbs') + .replaceRoot('wbs'); + + res.status(200).send(result); + } catch (error) { + res.status(404).send(error); + } }; return { @@ -92,6 +114,7 @@ const wbsController = function (WBS) { getAllWBS, getWBS, getWBSById, + getWBSByUserId, }; }; diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index f0f69e146..77c5ca0b7 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -7,6 +7,7 @@ const userProfileJobs = () => { const allUserProfileJobs = new CronJob( // '* * * * *', // Comment out for testing. Run Every minute. '1 0 * * 0', // Every Sunday, 1 minute past midnight. + // '30 22 * * 0', // hotfix for 10:30pm async () => { const SUNDAY = 0; // will change back to 0 after fix diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 533dbe367..80422f153 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -2,17 +2,28 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const timeentry = require('../models/timeentry'); +const myTeam = require('./helperModels/myTeam'); const team = require('../models/team'); const { hasPermission } = require('../utilities/permissions'); + const dashboardhelper = function () { const personaldetails = function (userId) { - return userProfile.findById(userId, '_id firstName lastName role profilePic badgeCollection'); + return userProfile.findById( + userId, + "_id firstName lastName role profilePic badgeCollection" + ); }; const getOrgData = async function () { - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + const pdtstart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); + const pdtend = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); /** * Previous aggregate pipeline had two issues: @@ -31,41 +42,43 @@ const dashboardhelper = function () { $gte: 1, }, role: { - $ne: 'Mentor', + $ne: "Mentor", }, }, }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { $project: { - personId: '$_id', + personId: "$_id", name: 1, weeklycommittedHours: 1, role: 1, - endDate: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $gte: ['$$timeentry.dateOfWork', pdtstart], + $gte: ["$$timeentry.dateOfWork", pdtstart], }, { - $lte: ['$$timeentry.dateOfWork', pdtend], + $lte: ["$$timeentry.dateOfWork", pdtend], }, { $not: [ { - $in: ['$$timeentry.entryType', ['person', 'team', 'project']], + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], }, ], }, @@ -77,7 +90,7 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, @@ -85,31 +98,30 @@ const dashboardhelper = function () { $project: { personId: 1, weeklycommittedHours: 1, - endDate: 1, totalSeconds: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, tangibletime: { $cond: [ { - $eq: ['$timeEntryData.isTangible', true], + $eq: ["$timeEntryData.isTangible", true], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, intangibletime: { $cond: [ { - $eq: ['$timeEntryData.isTangible', false], + $eq: ["$timeEntryData.isTangible", false], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, @@ -118,17 +130,17 @@ const dashboardhelper = function () { { $group: { _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', + personId: "$personId", + weeklycommittedHours: "$weeklycommittedHours", }, time_hrs: { - $sum: { $divide: ['$totalSeconds', 3600] }, + $sum: { $divide: ["$totalSeconds", 3600] }, }, tangibletime_hrs: { - $sum: { $divide: ['$tangibletime', 3600] }, + $sum: { $divide: ["$tangibletime", 3600] }, }, intangibletime_hrs: { - $sum: { $divide: ['$intangibletime', 3600] }, + $sum: { $divide: ["$intangibletime", 3600] }, }, }, }, @@ -136,15 +148,15 @@ const dashboardhelper = function () { $group: { _id: 0, memberCount: { $sum: 1 }, - totalweeklycommittedHours: { $sum: '$_id.weeklycommittedHours' }, + totalweeklycommittedHours: { $sum: "$_id.weeklycommittedHours" }, totaltime_hrs: { - $sum: '$time_hrs', + $sum: "$time_hrs", }, totaltangibletime_hrs: { - $sum: '$tangibletime_hrs', + $sum: "$tangibletime_hrs", }, totalintangibletime_hrs: { - $sum: '$intangibletime_hrs', + $sum: "$intangibletime_hrs", }, }, }, @@ -156,39 +168,39 @@ const dashboardhelper = function () { const getLeaderboard = async function (userId) { const userid = mongoose.Types.ObjectId(userId); try { - const userById = await userProfile.findOne({ _id: userid, isActive: true }, { role: 1 }); + const userById = await userProfile.findOne( + { _id: userid, isActive: true }, + { role: 1 } + ); if (userById == null) return null; const userRole = userById.role; - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtstart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + const pdtend = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); let teamMemberIds = [userid]; let teamMembers = []; - const userAsRequestor = { role: userRole, requestorId: userId }; + const userAsRequestor = {'role': userRole, requestorId: userId }; const canSeeUsersInDashboard = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); if (!canSeeUsersInDashboard) { // Manager , Mentor , Volunteer ... , Show only team members const teamsResult = await team.find( - { 'members.userId': { $in: [userid] } }, - { members: 1 }, + { "members.userId": { $in: [userid] } }, + { members: 1 } ); - console.log(teamsResult); teamsResult.forEach((_myTeam) => { - let isUserVisible = false; _myTeam.members.forEach((teamMember) => { - if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; - }); - if(isUserVisible) - { - _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); }); - } - }); teamMembers = await userProfile.find( @@ -202,9 +214,7 @@ const dashboardhelper = function () { weeklySummaries: 1, timeOffFrom: 1, timeOffTill: 1, - endDate: 1, } - ); } else { // 'Core Team', 'Owner' //All users @@ -219,13 +229,11 @@ const dashboardhelper = function () { weeklySummaries: 1, timeOffFrom: 1, timeOffTill: 1, - endDate: 1, - - }, + } ); } - teamMemberIds = teamMembers.map((member) => member._id); + teamMemberIds = teamMembers.map(member => member._id); const timeEntries = await timeentry.find({ dateOfWork: { @@ -233,7 +241,6 @@ const dashboardhelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, - isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -249,9 +256,11 @@ const dashboardhelper = function () { } if (timeEntry.isTangible === true) { - timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += + timeEntry.totalSeconds; } else { - timeEntryByPerson[personIdStr].intangibleSeconds += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].intangibleSeconds += + timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; @@ -266,26 +275,28 @@ const dashboardhelper = function () { isVisible: teamMember.isVisible, hasSummary: teamMember.weeklySummaries?.length > 0 - ? teamMember.weeklySummaries[0].summary !== '' + ? teamMember.weeklySummaries[0].summary !== "" : false, weeklycommittedHours: teamMember.weeklycommittedHours, totaltangibletime_hrs: - (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds ?? 0) / 3600, + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + 3600 || 0, totalintangibletime_hrs: - (timeEntryByPerson[teamMember._id.toString()]?.intangibleSeconds ?? 0) / 3600, - totaltime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds ?? 0) / 3600, - + timeEntryByPerson[teamMember._id.toString()]?.intangibleSeconds / + 3600 || 0, + totaltime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || + 0, percentagespentintangible: timeEntryByPerson[teamMember._id.toString()] && timeEntryByPerson[teamMember._id.toString()]?.totalSeconds !== 0 && timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds !== 0 - ? ((timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds || 0) / - (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds || 1)) * + ? (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds) * 100 : 0, timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, - endDate: teamMember.endDate || null, }; leaderBoardData.push(obj); }); @@ -567,9 +578,15 @@ const dashboardhelper = function () { */ const getUserLaborData = async function (userId) { try { - const pdtStart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtStart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); - const pdtEnd = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + const pdtEnd = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); const user = await userProfile.findById({ _id: userId, @@ -580,8 +597,7 @@ const dashboardhelper = function () { $gte: pdtStart, $lte: pdtEnd, }, - entryType: { $in: ['default', null] }, - isActive: { $ne: false }, + entryType: { $in: ["default", null] }, personId: userId, }); @@ -601,23 +617,23 @@ const dashboardhelper = function () { personId: userId, role: user.role, isVisible: user.isVisible, - hasSummary: user.weeklySummaries[0].summary !== '', + hasSummary: user.weeklySummaries[0].summary !== "", weeklycommittedHours: user.weeklycommittedHours, name: `${user.firstName} ${user.lastName}`, totaltime_hrs: (tangibleSeconds + intangibleSeconds) / 3600, totaltangibletime_hrs: tangibleSeconds / 3600, totalintangibletime_hrs: intangibleSeconds / 3600, - percentagespentintangible: (intangibleSeconds / tangibleSeconds) * 100, + percentagespentintangible: + (intangibleSeconds / tangibleSeconds) * 100, timeOffFrom: user.timeOffFrom, timeOffTill: user.timeOffTill, - endDate: user.endDate || null, }, ]; } catch (err) { return [ { - personId: 'error', - name: 'Error Error', + personId: "error", + name: "Error Error", totaltime_hrs: 0, totaltangibletime_hrs: 0, totalintangibletime_hrs: 0, @@ -628,8 +644,8 @@ const dashboardhelper = function () { }; const laborthismonth = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return timeentry.aggregate([ { @@ -645,19 +661,19 @@ const dashboardhelper = function () { { $group: { _id: { - projectId: '$projectId', + projectId: "$projectId", }, labor: { - $sum: '$totalSeconds', + $sum: "$totalSeconds", }, }, }, { $lookup: { - from: 'projects', - localField: '_id.projectId', - foreignField: '_id', - as: 'project', + from: "projects", + localField: "_id.projectId", + foreignField: "_id", + as: "project", }, }, { @@ -666,13 +682,13 @@ const dashboardhelper = function () { projectName: { $ifNull: [ { - $arrayElemAt: ['$project.projectName', 0], + $arrayElemAt: ["$project.projectName", 0], }, - 'Undefined', + "Undefined", ], }, timeSpent_hrs: { - $divide: ['$labor', 3600], + $divide: ["$labor", 3600], }, }, }, @@ -680,8 +696,8 @@ const dashboardhelper = function () { }; const laborthisweek = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return userProfile.aggregate([ { @@ -697,10 +713,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -708,23 +724,26 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $eq: ['$$timeentry.isTangible', true], + $eq: ["$$timeentry.isTangible", true], }, { - $gte: ['$$timeentry.dateOfWork', fromdate], + $gte: ["$$timeentry.dateOfWork", fromdate], }, { - $lte: ['$$timeentry.dateOfWork', todate], + $lte: ["$$timeentry.dateOfWork", todate], }, { $not: [ { - $in: ['$$timeentry.entryType', ['person', 'team', 'project']], + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], }, ], }, @@ -736,27 +755,27 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, { $group: { _id: { - _id: '$_id', - weeklycommittedHours: '$weeklycommittedHours', + _id: "$_id", + weeklycommittedHours: "$weeklycommittedHours", }, effort: { - $sum: '$timeEntryData.totalSeconds', + $sum: "$timeEntryData.totalSeconds", }, }, }, { $project: { _id: 0, - weeklycommittedHours: '$_id.weeklycommittedHours', + weeklycommittedHours: "$_id.weeklycommittedHours", timeSpent_hrs: { - $divide: ['$effort', 3600], + $divide: ["$effort", 3600], }, }, }, @@ -764,8 +783,8 @@ const dashboardhelper = function () { }; const laborThisWeekByCategory = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format('YYYY-MM-DD'); - const todate = moment(endDate).format('YYYY-MM-DD'); + const fromdate = moment(startDate).format("YYYY-MM-DD"); + const todate = moment(endDate).format("YYYY-MM-DD"); return userProfile.aggregate([ { @@ -781,10 +800,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "_id", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -792,23 +811,26 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $eq: ['$$timeentry.isTangible', true], + $eq: ["$$timeentry.isTangible", true], }, { - $gte: ['$$timeentry.dateOfWork', fromdate], + $gte: ["$$timeentry.dateOfWork", fromdate], }, { - $lte: ['$$timeentry.dateOfWork', todate], + $lte: ["$$timeentry.dateOfWork", todate], }, { $not: [ { - $in: ['$$timeentry.entryType', ['person', 'team', 'project']], + $in: [ + "$$timeentry.entryType", + ["person", "team", "project"], + ], }, ], }, @@ -820,37 +842,37 @@ const dashboardhelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: '$timeEntryData.projectId', + _id: "$timeEntryData.projectId", effort: { - $sum: '$timeEntryData.totalSeconds', + $sum: "$timeEntryData.totalSeconds", }, }, }, { $lookup: { - from: 'projects', - localField: '_id', - foreignField: '_id', - as: 'project', + from: "projects", + localField: "_id", + foreignField: "_id", + as: "project", }, }, { $unwind: { - path: '$project', + path: "$project", preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: '$project.category', + _id: "$project.category", effort: { - $sum: '$effort', + $sum: "$effort", }, }, }, @@ -858,7 +880,7 @@ const dashboardhelper = function () { $project: { _id: 1, timeSpent_hrs: { - $divide: ['$effort', 3600], + $divide: ["$effort", 3600], }, }, }, diff --git a/src/helpers/helperModels/userProjects.js b/src/helpers/helperModels/userProjects.js new file mode 100644 index 000000000..a2f1f2b5e --- /dev/null +++ b/src/helpers/helperModels/userProjects.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const ProjectSchema = new Schema({ + projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'projects' }, + projectName: { type: String }, + category: { type: String }, +}); + +const userProjectSchema = new Schema({ + + _id: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + projects: [ProjectSchema], +}); + +module.exports = mongoose.model('userProject', userProjectSchema, 'userProjects'); diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js deleted file mode 100644 index 52d6a2ad0..000000000 --- a/src/helpers/overviewReportHelper.js +++ /dev/null @@ -1,645 +0,0 @@ -/* eslint-disable no-plusplus */ -/* eslint-disable quotes */ -const Team = require('../models/team'); -const UserProfile = require('../models/userProfile'); -const TimeEntries = require('../models/timeentry'); -const Task = require('../models/task'); - -const overviewReportHelper = function () { - /** - * Get map location statistics - * Group and count all volunteers by their lattitude and longitude - */ - async function getMapLocations() { - return UserProfile.aggregate([ - { - $match: { - isActive: true, - 'location.coords.lat': { $ne: null }, - 'location.coords.lng': { $ne: null }, - }, - }, - { - $group: { - _id: { - lat: '$location.coords.lat', - lng: '$location.coords.lng', - }, - count: { $sum: 1 }, - }, - }, - ]); - } - - /** - * Get the total number of active teams - */ - async function getTotalActiveTeamCount() { - return Team.aggregate([ - { - $match: { - isActive: true, - }, - }, - { - $count: 'activeTeams', - }, - ]); - } - - /** - * Get the users celebrating their anniversary between the two input dates. - * @param {*} startDate - * @param {*} endDate - * @returns The number of users celebrating their anniversary between the two input dates. - */ - async function getAnniversaries(startDate, endDate) { - return UserProfile.aggregate([ - { - $addFields: { - createdMonthDay: { $dateToString: { format: '%m-%d', date: '$createdDate' } }, - }, - }, - { - $match: { - createdMonthDay: { - $gte: startDate.substring(5, 10), - $lte: endDate.substring(5, 10), - }, - isActive: true, - }, - }, - { - $project: { - _id: 1, - firstName: 1, - lastName: 1, - }, - }, - ]); - } - - /** - * Get the number of Blue Square infringements between the two input dates. - * @param {*} startDate - * @param {*} endDate - * @returns - */ - async function getBlueSquareStats(startDate, endDate) { - return UserProfile.aggregate([ - { - $unwind: '$infringements', - }, - { - $match: { - 'infringements.date': { - $gte: startDate, - $lte: endDate, - }, - }, - }, - { - $group: { - _id: '$infringements.description', - count: { $sum: 1 }, - }, - }, - ]); - } - - /** - * Get the number of members in team and not in team, with percentage - */ - async function getTeamMembersCount() { - const [data] = await UserProfile.aggregate([ - { - $match: { - isActive: true, - }, - }, - { - $facet: { - totalMembers: [ - { - $group: { - _id: null, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - count: 1, - }, - }, - ], - - inTeam: [ - { - $match: { - teams: { - $exists: true, - $ne: [], - }, - }, - }, - { - $count: 'usersInTeam', - }, - ], - }, - }, - ]); - - return data; - } - - /** aggregates role distribution statistics - * counts total number of volunteers that fall within each of the different roles - */ - async function getRoleDistributionStats() { - const roleStats = UserProfile.aggregate([ - { - $match: { isActive: true }, - }, - { - $group: { - _id: '$role', - count: { $sum: 1 }, - }, - }, - ]); - - return roleStats; - } - - /** - * aggregates the total number of hours worked between the 5 categories - * Food, Energy, Housing, Stewardship, Society, Economics and Other - */ - async function getWorkDistributionStats(startDate, endDate) { - const distributionStats = TimeEntries.aggregate([ - { - $match: { - dateOfWork: { $gte: startDate, $lte: endDate }, - }, - }, - { - $lookup: { - from: 'projects', - localField: 'projectId', - foreignField: '_id', - as: 'project', - }, - }, - { - $unwind: { - path: '$project', - preserveNullAndEmptyArrays: true, - }, - }, - { - $group: { - _id: '$project.category', - aggregatedSeconds: { $sum: '$totalSeconds' }, - }, - }, - { - $project: { - _id: 1, - totalHours: { $divide: ['$aggregatedSeconds', 3600] }, - }, - }, - ]); - - return distributionStats; - } - - async function getTasksStats(startDate, endDate) { - const taskStats = await Task.aggregate([ - { - $match: { - modifiedDatetime: { $gte: startDate, $lte: endDate }, - status: { $in: ['Complete', 'Active'] }, - }, - }, - { - $group: { - _id: '$status', - count: { $sum: 1 }, - }, - }, - ]); - - if (!taskStats.find((x) => x._id === 'Active')) { - taskStats.push({ _id: 'Active', count: 0 }); - } - if (!taskStats.find((x) => x._id === 'Complete')) { - taskStats.push({ _id: 'Complete', count: 0 }); - } - - return taskStats; - } - /** - * Get the volunteer hours stats, it retrieves the number of hours logged by users between the two input dates as well as their weeklycommittedHours. - * @param {*} startDate - * @param {*} endDate - */ - async function getHoursStats(startDate, endDate) { - const hoursStats = await UserProfile.aggregate([ - { - $match: { - isActive: true, - }, - }, - { - $lookup: { - from: 'timeEntries', // The collection to join - localField: '_id', // Field from the userProfile collection - foreignField: 'personId', // Field from the timeEntries collection - as: 'timeEntries', // The array field that will contain the joined documents - }, - }, - { - $unwind: { - path: '$timeEntries', - preserveNullAndEmptyArrays: true, // Preserve users with no time entries - }, - }, - { - $match: { - $or: [ - { timeEntries: { $exists: false } }, - { 'timeEntries.dateOfWork': { $gte: startDate, $lte: endDate } }, - ], - }, - }, - { - $group: { - _id: '$_id', - personId: { $first: '$_id' }, - totalSeconds: { $sum: '$timeEntries.totalSeconds' }, // Sum seconds from timeEntries - weeklycommittedHours: { $first: `$weeklycommittedHours` }, // Include the weeklycommittedHours field - }, - }, - { - $project: { - totalHours: { $divide: ['$totalSeconds', 3600] }, // Convert seconds to hours - weeklycommittedHours: 1, // make sure we include it in the end result - }, - }, - { - $bucket: { - groupBy: '$totalHours', - boundaries: [0, 10, 20, 30, 40], - default: 40, - output: { - count: { $sum: 1 }, - }, - }, - }, - ]); - for (let i = 0; i < 5; i++) { - if (!hoursStats.find((x) => x._id === i * 10)) { - hoursStats.push({ _id: i * 10, count: 0 }); - } - } - return hoursStats; - } - - /** - * Aggregates total number of hours worked across all volunteers within the specified date range - */ - async function getTotalHoursWorked(startDate, endDate) { - console.log(startDate, endDate); - const data = await TimeEntries.aggregate([ - { - $match: { - dateOfWork: { $gte: startDate, $lte: endDate }, - }, - }, - { - $group: { - _id: null, - totalSeconds: { $sum: '$totalSeconds' }, - }, - }, - { - $project: { - _id: 0, - totalHours: { $divide: ['$totalSeconds', 3600] }, - }, - }, - ]); - - return data; - } - - /** - * returns the number of: - * 1. Active volunteers - * 2. Volunteers that deactivated in the current week - * 3. New volunteers in the current week - * - * @param {string} startDate - * @param {string} endDate - */ - const getVolunteerNumberStats = async (startDate, endDate) => { - const [data] = await UserProfile.aggregate([ - { - $facet: { - activeVolunteers: [{ $match: { isActive: true } }, { $count: 'activeVolunteersCount' }], - - newVolunteers: [ - { - $match: { - createdDate: { - $gte: startDate, - $lte: endDate, - }, - }, - }, - { $count: 'newVolunteersCount' }, - ], - - deactivatedVolunteers: [ - { - $match: { - $and: [ - { lastModifiedDate: { $gte: startDate } }, - { lastModifiedDate: { $lte: endDate } }, - { isActive: false }, - ], - }, - }, - { $count: 'deactivedVolunteersCount' }, - ], - }, - }, - ]); - - return data; - }; - - /** - * - * @returns The number of teams with 4 or more members. - */ - async function getFourPlusMembersTeamCount() { - // check if members array has 4 or more members - return Team.countDocuments({ 'members.4': { $exists: true } }); - } - - /** - * Get the total number of badges awarded between the two input dates. - * @param {*} startDate - * @param {*} endDate - * @returns The total number of badges awarded between the two input dates. - */ - async function getTotalBadgesAwardedCount(startDate, endDate) { - return UserProfile.aggregate([ - { - $unwind: '$badgeCollection', - }, - { - $match: { - 'badgeCollection.earnedDate': { - $gte: startDate, - $lte: endDate, - }, - }, - }, - { - $count: 'badgeCollection', - }, - ]); - } - - /** - * Get the number of users celebrating their anniversary between the two input dates. - * @param {*} startDate - * @param {*} endDate - * @returns The number of users celebrating their anniversary between the two input dates. - */ - async function getAnniversaryCount(startDate, endDate) { - return UserProfile.aggregate([ - { - $addFields: { - createdMonthDay: { $dateToString: { format: '%m-%d', date: '$createdDate' } }, - }, - }, - { - $match: { - createdMonthDay: { - $gte: new Date(startDate).toISOString().substring(5, 10), - $lte: new Date(endDate).toISOString().substring(5, 10), - }, - }, - }, - { - $count: 'anniversaryCount', - }, - ]); - } - - /** - * Get the role and count of users. - * @returns The role and count of users. - */ - async function getRoleCount() { - return UserProfile.aggregate([ - { - $group: { - _id: '$role', - count: { $sum: 1 }, - }, - }, - ]); - } - - /** - * Get the number of active and inactive users. - */ - async function getActiveInactiveUsersCount() { - const activeUsers = await UserProfile.countDocuments({ isActive: true }); - const inactiveUsers = await UserProfile.countDocuments({ isActive: false }); - - return { - activeUsers, - inactiveUsers, - }; - } - - /** - * Groups users based off of hours logged and the percentage of hours logged divided by their weeklycommittedHours for the current week and last week. - * @param {*} startDate - * @param {*} endDate - */ - async function getVolunteerHoursStats(startDate, endDate, lastWeekStartDate, lastWeekEndDate) { - const currentWeekStats = await getHoursStats(startDate, endDate); - const lastWeekStats = await getHoursStats(lastWeekStartDate, lastWeekEndDate); - - const volunteerHoursStats = { - numberOfUsers: currentWeekStats.length, - }; - - // - const percentageWorkedStats = { - thisWeek: { '<100': 0, '100-109': 0, '110-149': 0, '150-199': 0, '200+': 0 }, - lastWeek: { '<100': 0, '100-109': 0, '110-149': 0, '150-199': 0, '200+': 0 }, - }; - - for (let i = 0; i < 6; i++) { - const group = i * 10; - volunteerHoursStats[`${group}-${group + 9}`] = 0; - } - volunteerHoursStats['60+'] = 0; - - // Group users by the number of hours logged as well as percentage of weeklycommittedHours worked - currentWeekStats.forEach((user) => { - if (user.totalHours >= 60) { - volunteerHoursStats['60+'] = volunteerHoursStats['60+'] - ? volunteerHoursStats['60+'] + 1 - : 1; - console.log('user with 60+ hours'); - } else { - const group = Math.floor(user.totalHours / 10) * 10; - volunteerHoursStats[`${group}-${group + 9}`] += 1; - } - - const percentage = user.totalHours / user.weeklycommittedHours; - - if (percentage < 1) { - percentageWorkedStats.thisWeek['<100'] += 1; - } else if (percentage < 1.1) { - percentageWorkedStats.thisWeek['100-109'] += 1; - } else if (percentage < 1.5) { - percentageWorkedStats.thisWeek['110-149'] += 1; - } else if (percentage < 2) { - percentageWorkedStats.thisWeek['150-199'] += 1; - } else { - percentageWorkedStats.thisWeek['200+'] += 1; - } - }); - - // now we need to group last weeks statistics by percentage of weeklycommittedHours worked - lastWeekStats.forEach((user) => { - const percentage = user.totalHours / user.weeklycommittedHours; - if (percentage < 1) { - percentageWorkedStats.lastWeek['<100'] += 1; - } else if (percentage < 1.1) { - percentageWorkedStats.lastWeek['100-109'] += 1; - } else if (percentage < 1.5) { - percentageWorkedStats.lastWeek['110-149'] += 1; - } else if (percentage < 2) { - percentageWorkedStats.lastWeek['150-199'] += 1; - } else { - percentageWorkedStats.lastWeek['200+'] += 1; - } - }); - - return { volunteerHoursStats, percentageWorkedStats }; - } - - /** - * 1. Total hours logged in tasks - * 2. Total hours logged in projects - * 3. Number of member with tasks assigned - * 4. Number of member without tasks assigned - * 5. Number of tasks with due date within the date range - * @param {*} startDate - * @param {*} endDate - */ - async function getTaskAndProjectStats(startDate, endDate) { - // 1. Total hours logged in tasks - const taskHours = await TimeEntries.aggregate([ - { - $match: { - dateOfWork: { $gte: startDate, $lte: endDate }, - taskId: { $exists: true }, - }, - }, - { - $group: { - _id: null, - totalSeconds: { $sum: '$totalSeconds' }, - }, - }, - { - $project: { - totalHours: { $divide: ['$totalSeconds', 3600] }, - }, - }, - ]); - - // 2. Total hours logged in projects - const projectHours = await TimeEntries.aggregate([ - { - $match: { - dateOfWork: { $gte: startDate, $lte: endDate }, - projectId: { $exists: true }, - }, - }, - { - $group: { - _id: null, - totalSeconds: { $sum: '$totalSeconds' }, - }, - }, - { - $project: { - totalHours: { $divide: ['$totalSeconds', 3600] }, - }, - }, - ]); - - // 3. Number of member with tasks assigned - const membersWithTasks = await Task.distinct('resources.userID', { - 'resources.userID': { $exists: true }, - completedTask: { $ne: true }, - }); - - // 4. Number of member without tasks assigned - const membersWithoutTasks = await UserProfile.countDocuments({ - _id: { $nin: membersWithTasks }, - }); - - // 5. Number of tasks with due date within the date range - const tasksDueWithinDate = await Task.countDocuments({ - dueDatetime: { $gte: startDate, $lte: endDate }, - }); - - const taskAndProjectStats = { - taskHours: taskHours[0].totalHours.toFixed(2), - projectHours: projectHours[0].totalHours.toFixed(2), - membersWithTasks: membersWithTasks.length, - membersWithoutTasks, - tasksDueThisWeek: tasksDueWithinDate, - }; - - return taskAndProjectStats; - } - - return { - getMapLocations, - getTotalActiveTeamCount, - getAnniversaries, - getRoleDistributionStats, - getVolunteerNumberStats, - getTasksStats, - getWorkDistributionStats, - getTotalHoursWorked, - getHoursStats, - getFourPlusMembersTeamCount, - getTotalBadgesAwardedCount, - getAnniversaryCount, - getRoleCount, - getBlueSquareStats, - getTeamMembersCount, - getActiveInactiveUsersCount, - getVolunteerHoursStats, - getTaskAndProjectStats, - }; -}; - -module.exports = overviewReportHelper; diff --git a/src/helpers/overviewReportHelper.spec.js b/src/helpers/overviewReportHelper.spec.js deleted file mode 100644 index 44fb7bf83..000000000 --- a/src/helpers/overviewReportHelper.spec.js +++ /dev/null @@ -1,64 +0,0 @@ -const overviewReportHelper = require('./overviewReportHelper'); -const UserProfile = require('../models/userProfile'); - -const makeSut = () => { - const { getVolunteerNumberStats } = overviewReportHelper(); - - return { getVolunteerNumberStats }; -}; - -describe('overviewReportHelper method tests', () => { - const startDate = '2024-05-26T00:00:00Z'; - const endDate = '2024-06-02T00:00:00Z'; - - describe('getVolunteerNumberStats method', () => { - test('it should call the aggregation method on UserProfile', async () => { - const { getVolunteerNumberStats } = makeSut(); - const aggregateSpy = jest.spyOn(UserProfile, 'aggregate').mockImplementationOnce(() => null); - - await getVolunteerNumberStats(startDate, endDate); - - expect(aggregateSpy).toHaveBeenCalled(); - }); - - test('it should call the aggregation query with the correct parameters', async () => { - const { getVolunteerNumberStats } = makeSut(); - const aggregateSpy = jest.spyOn(UserProfile, 'aggregate').mockImplementationOnce(() => null); - - await getVolunteerNumberStats(startDate, endDate); - - expect(aggregateSpy).toHaveBeenCalled(); - expect(aggregateSpy).toHaveBeenCalledWith([ - { - $facet: { - activeVolunteers: [{ $match: { isActive: true } }, { $count: 'activeVolunteersCount' }], - - newVolunteers: [ - { - $match: { - createdDate: { - $gte: startDate, - $lte: endDate, - }, - }, - }, - { $count: 'newVolunteersCount' }, - ], - - deactivatedVolunteers: [ - { - $match: { - $and: [ - { lastModifiedDate: { $gte: startDate } }, - { lastModifiedDate: { $lte: endDate } }, - { isActive: false }, - ], - }, - }, - ], - }, - }, - ]); - }); - }); -}); diff --git a/src/helpers/taskHelper.js b/src/helpers/taskHelper.js index 34fb36be8..dca64cb66 100644 --- a/src/helpers/taskHelper.js +++ b/src/helpers/taskHelper.js @@ -11,6 +11,7 @@ const taskHelper = function () { const getTasksForTeams = async function (userId, requestor) { const userid = mongoose.Types.ObjectId(userId); const requestorId = mongoose.Types.ObjectId(requestor.requestorId); + const requestorRole = requestor.role; try { const userById = await userProfile.findOne( { _id: userid, isActive: true }, @@ -21,128 +22,103 @@ const taskHelper = function () { isVisible: 1, weeklycommittedHours: 1, weeklySummaries: 1, - weeklySummaryOption: 1, timeOffFrom: 1, timeOffTill: 1, - teamCode: 1, - teams: 1, adminLinks: 1, - }, + } ); if (userById === null) return null; const userRole = userById.role; - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + const pdtstart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); + const pdtend = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); let teamMemberIds = [userid]; let teamMembers = []; const isRequestorOwnerLike = await hasPermission(requestor, 'seeUsersInDashboard'); - const userAsRequestor = { role: userRole, requestorId: userId }; + const userAsRequestor = {'role': userRole, requestorId: userId }; const isUserOwnerLike = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); switch (true) { case isRequestorOwnerLike && isUserOwnerLike: { - teamMembers = await userProfile - .find( - { isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - weeklySummaryOption: 1, - timeOffFrom: 1, - timeOffTill: 1, - teamCode: 1, - teams: 1, - adminLinks: 1, - }, - ) - .populate([ - { - path: 'teams', - select: 'teamName', - }, - ]); + teamMembers = await userProfile.find( + { isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + adminLinks: 1, + } + ); break; } case isRequestorOwnerLike && !isUserOwnerLike: { const teamsResult = await team.find( - { 'members.userId': { $in: [userid] } }, - { members: 1 }, + { "members.userId": { $in: [userid] } }, + { members: 1 } ); teamsResult.forEach((_myTeam) => { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); }); }); - teamMembers = await userProfile - .find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - weeklySummaryOption: 1, - timeOffFrom: 1, - timeOffTill: 1, - teamCode: 1, - teams: 1, - adminLinks: 1, - }, - ) - .populate([ - { - path: 'teams', - select: 'teamName', - }, - ]); + teamMembers = await userProfile.find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + adminLinks: 1, + } + ); break; } default: { const sharedTeamsResult = await team.find( - { 'members.userId': { $all: [userid, requestorId] } }, - { members: 1 }, + { "members.userId": { $all: [userid, requestorId] } }, + { members: 1 } ); sharedTeamsResult.forEach((_myTeam) => { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); }); }); - teamMembers = await userProfile - .find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - weeklySummaryOption: 1, - timeOffFrom: 1, - timeOffTill: 1, - teamCode: 1, - teams: 1, - adminLinks: 1, - }, - ) - .populate([ - { - path: 'teams', - select: 'teamName', - }, - ]); + teamMembers = await userProfile.find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + timeOffFrom: 1, + timeOffTill: 1, + adminLinks: 1, + } + ); } } - teamMemberIds = teamMembers.map((member) => member._id); + teamMemberIds = teamMembers.map(member => member._id); const timeEntries = await timeentry.find({ dateOfWork: { @@ -150,7 +126,6 @@ const taskHelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, - isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -164,18 +139,19 @@ const taskHelper = function () { }; } if (timeEntry.isTangible) { - timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += + timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; }); const teamMemberTasks = await Task.find( - { 'resources.userID': { $in: teamMemberIds } }, - { 'resources.profilePic': 0 }, + { "resources.userID": { $in: teamMemberIds } }, + { "resources.profilePic": 0 } ).populate({ - path: 'wbsId', - select: 'projectId', + path: "wbsId", + select: "projectId", }); - const teamMemberTaskIds = teamMemberTasks.map((task) => task._id); + const teamMemberTaskIds = teamMemberTasks.map(task => task._id); const teamMemberTaskNotifications = await TaskNotification.find({ taskId: { $in: teamMemberTaskIds }, }); @@ -187,9 +163,13 @@ const taskHelper = function () { const taskNdUserID = `${taskIdStr},${userIdStr}`; if (taskNotificationByTaskNdUser[taskNdUserID]) { - taskNotificationByTaskNdUser[taskNdUserID].push(teamMemberTaskNotification); + taskNotificationByTaskNdUser[taskNdUserID].push( + teamMemberTaskNotification + ); } else { - taskNotificationByTaskNdUser[taskNdUserID] = [teamMemberTaskNotification]; + taskNotificationByTaskNdUser[taskNdUserID] = [ + teamMemberTaskNotification, + ]; } }); @@ -203,11 +183,8 @@ const taskHelper = function () { teamMemberTask.resources.forEach((resource) => { const resourceIdStr = resource.userID?.toString(); const taskNdUserID = `${taskIdStr},${resourceIdStr}`; - // initialize taskNotifications if not exists - if (!_teamMemberTask.taskNotifications) _teamMemberTask.taskNotifications = []; - // push all notifications into the list if taskNdUserId key exists - if (taskNotificationByTaskNdUser[taskNdUserID]) - _teamMemberTask.taskNotifications.push(...taskNotificationByTaskNdUser[taskNdUserID]); + _teamMemberTask.taskNotifications = + taskNotificationByTaskNdUser[taskNdUserID] || []; if (taskByPerson[resourceIdStr]) { taskByPerson[resourceIdStr].push(_teamMemberTask); } else { @@ -218,22 +195,20 @@ const taskHelper = function () { const teamMemberTasksData = []; teamMembers.forEach((teamMember) => { - const timeEntry = timeEntryByPerson[teamMember._id.toString()]; - const tangible = timeEntry?.tangibleSeconds || 0; - const total = timeEntry?.totalSeconds || 0; const obj = { personId: teamMember._id, role: teamMember.role, name: `${teamMember.firstName} ${teamMember.lastName}`, weeklycommittedHours: teamMember.weeklycommittedHours, - weeklySummaryOption: teamMember.weeklySummaryOption || null, - totaltangibletime_hrs: tangible / 3600, - totaltime_hrs: total / 3600, + totaltangibletime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / + 3600 || 0, + totaltime_hrs: + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || + 0, tasks: taskByPerson[teamMember._id.toString()] || [], timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, - teamCode: teamMember.teamCode || null, - teams: teamMember.teams || null, adminLinks: teamMember.adminLinks || null, }; teamMemberTasksData.push(obj); @@ -529,8 +504,14 @@ const taskHelper = function () { // ]); }; const getTasksForSingleUser = function (userId) { - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + const pdtstart = moment() + .tz("America/Los_Angeles") + .startOf("week") + .format("YYYY-MM-DD"); + const pdtend = moment() + .tz("America/Los_Angeles") + .endOf("week") + .format("YYYY-MM-DD"); return userProfile.aggregate([ { $match: { @@ -539,33 +520,33 @@ const taskHelper = function () { }, { $project: { - personId: '$_id', - role: '$role', + personId: "$_id", + role: "$role", name: { - $concat: ['$firstName', ' ', '$lastName'], + $concat: ["$firstName", " ", "$lastName"], }, weeklycommittedHours: { $sum: [ - '$weeklycommittedHours', + "$weeklycommittedHours", { - $ifNull: ['$missedHours', 0], + $ifNull: ["$missedHours", 0], }, ], }, timeOffFrom: { - $ifNull: ['$timeOffFrom', null], + $ifNull: ["$timeOffFrom", null], }, timeOffTill: { - $ifNull: ['$timeOffTill', null], + $ifNull: ["$timeOffTill", null], }, }, }, { $lookup: { - from: 'timeEntries', - localField: 'personId', - foreignField: 'personId', - as: 'timeEntryData', + from: "timeEntries", + localField: "personId", + foreignField: "personId", + as: "timeEntryData", }, }, { @@ -578,21 +559,18 @@ const taskHelper = function () { role: 1, timeEntryData: { $filter: { - input: '$timeEntryData', - as: 'timeentry', + input: "$timeEntryData", + as: "timeentry", cond: { $and: [ { - $gte: ['$$timeentry.dateOfWork', pdtstart], + $gte: ["$$timeentry.dateOfWork", pdtstart], }, { - $lte: ['$$timeentry.dateOfWork', pdtend], + $lte: ["$$timeentry.dateOfWork", pdtend], }, { - $in: ['$$timeentry.entryType', ['default', null]], - }, - { - $ne: ['$$timeentry.isActive', false], + $in: ["$$timeentry.entryType", ["default", null]], }, ], }, @@ -602,7 +580,7 @@ const taskHelper = function () { }, { $unwind: { - path: '$timeEntryData', + path: "$timeEntryData", preserveNullAndEmptyArrays: true, }, }, @@ -617,18 +595,18 @@ const taskHelper = function () { totalSeconds: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.totalSeconds', + "$timeEntryData.totalSeconds", 0, ], }, isTangible: { $cond: [ { - $gte: ['$timeEntryData.totalSeconds', 0], + $gte: ["$timeEntryData.totalSeconds", 0], }, - '$timeEntryData.isTangible', + "$timeEntryData.isTangible", false, ], }, @@ -639,9 +617,9 @@ const taskHelper = function () { tangibletime: { $cond: [ { - $eq: ['$isTangible', true], + $eq: ["$isTangible", true], }, - '$totalSeconds', + "$totalSeconds", 0, ], }, @@ -650,81 +628,76 @@ const taskHelper = function () { { $group: { _id: { - personId: '$personId', - weeklycommittedHours: '$weeklycommittedHours', - timeOffFrom: '$timeOffFrom', - timeOffTill: '$timeOffTill', - name: '$name', - role: '$role', + personId: "$personId", + weeklycommittedHours: "$weeklycommittedHours", + timeOffFrom: "$timeOffFrom", + timeOffTill: "$timeOffTill", + name: "$name", + role: "$role", }, totalSeconds: { - $sum: '$totalSeconds', + $sum: "$totalSeconds", }, tangibletime: { - $sum: '$tangibletime', + $sum: "$tangibletime", }, }, }, { $project: { _id: 0, - personId: '$_id.personId', - name: '$_id.name', - weeklycommittedHours: '$_id.weeklycommittedHours', - timeOffFrom: '$_id.timeOffFrom', - timeOffTill: '$_id.timeOffTill', - role: '$_id.role', + personId: "$_id.personId", + name: "$_id.name", + weeklycommittedHours: "$_id.weeklycommittedHours", + timeOffFrom: "$_id.timeOffFrom", + timeOffTill: "$_id.timeOffTill", + role: "$_id.role", totaltime_hrs: { - $divide: ['$totalSeconds', 3600], + $divide: ["$totalSeconds", 3600], }, totaltangibletime_hrs: { - $divide: ['$tangibletime', 3600], + $divide: ["$tangibletime", 3600], }, }, }, { $lookup: { - from: 'tasks', - localField: 'personId', - foreignField: 'resources.userID', - as: 'tasks', + from: "tasks", + localField: "personId", + foreignField: "resources.userID", + as: "tasks", }, }, { $project: { tasks: { - $filter: { - input: '$tasks', - as: 'task', - cond: { - $ne: ['$$task.isActive', false], - }, + resources: { + profilePic: 0, }, }, - 'tasks.resources.profilePic': 0, }, }, { $unwind: { - path: '$tasks', + path: "$tasks", preserveNullAndEmptyArrays: true, }, }, { $lookup: { - from: 'wbs', - localField: 'tasks.wbsId', - foreignField: '_id', - as: 'projectId', + from: "wbs", + localField: "tasks.wbsId", + foreignField: "_id", + as: "projectId", }, }, { $addFields: { - 'tasks.projectId': { + "tasks.projectId": { $cond: [ - { $ne: ['$projectId', []] }, - { $arrayElemAt: ['$projectId', 0] }, - '$tasks.projectId', + { $ne: ["$projectId", []] }, + { $arrayElemAt: ["$projectId", 0] }, + "$tasks.projectId", ], }, }, @@ -746,40 +719,40 @@ const taskHelper = function () { }, { $addFields: { - 'tasks.projectId': '$tasks.projectId.projectId', + "tasks.projectId": "$tasks.projectId.projectId", }, }, { $lookup: { - from: 'taskNotifications', - localField: 'tasks._id', - foreignField: 'taskId', - as: 'tasks.taskNotifications', + from: "taskNotifications", + localField: "tasks._id", + foreignField: "taskId", + as: "tasks.taskNotifications", }, }, { $group: { - _id: '$personId', - tasks: { $push: '$tasks' }, + _id: "$personId", + tasks: { $push: "$tasks" }, data: { - $first: '$$ROOT', + $first: "$$ROOT", }, }, }, { $addFields: { - 'data.tasks': { + "data.tasks": { $filter: { - input: '$tasks', - as: 'task', - cond: { $ne: ['$$task', {}] }, + input: "$tasks", + as: "task", + cond: { $ne: ["$$task", {}] }, }, }, }, }, { $replaceRoot: { - newRoot: '$data', + newRoot: "$data", }, }, ]); @@ -787,7 +760,7 @@ const taskHelper = function () { const getUserProfileFirstAndLastName = function (userId) { return userProfile.findById(userId).then((results) => { if (!results) { - return ' '; + return " "; } return `${results.firstName} ${results.lastName}`; }); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 4fbe3376e..634e7de99 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -22,7 +22,6 @@ const reportHelper = require('./reporthelper')(); const emailSender = require('../utilities/emailSender'); const logger = require('../startup/logger'); const token = require('../models/profileInitialSetupToken'); -const BlueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); const cache = require('../utilities/nodeCache')(); const timeOffRequest = require('../models/timeOffRequest'); const notificationService = require('../services/notificationService'); @@ -47,24 +46,6 @@ const userHelper = function () { }); }; - const getTeamManagementEmail = function (teamId) { - const parsedTeamId = mongoose.Types.ObjectId(teamId); - return userProfile - .find( - { - isActive: true, - teams: { - $in: [parsedTeamId], - }, - role: { - $in: ['Manager', 'Administrator'], - }, - }, - 'email role', - ) - .exec(); - }; - const getUserName = async function (userId) { const userid = mongoose.Types.ObjectId(userId); return userProfile.findById(userid, 'firstName lastName'); @@ -125,70 +106,26 @@ const userHelper = function () { coreTeamExtraHour, requestForTimeOffEmailBody, administrativeContent, - weeklycommittedHours, ) { let finalParagraph = ''; - let descrInfringement = ''; + if (timeRemaining === undefined) { finalParagraph = '

    Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.

    '; - descrInfringement = `

    Total Infringements: This is your ${moment - .localeData() - .ordinal(totalInfringements)} blue square of 5.

    `; } else { - let hrThisweek = weeklycommittedHours || 0 + coreTeamExtraHour; - const remainHr = timeRemaining || 0; - hrThisweek += remainHr; finalParagraph = `Please complete ALL owed time this week (${ - hrThisweek + totalInfringements - 5 + timeRemaining + coreTeamExtraHour } hours) to avoid receiving another blue square. If you have any questions about any of this, please see the "One Community Core Team Policies and Procedures" page.`; - descrInfringement = `

    Total Infringements: This is your ${moment - .localeData() - .ordinal( - totalInfringements, - )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your - requirement this week. This is in addition to any hours missed for last week: - ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours - owed for this being your ${moment - .localeData() - .ordinal( - totalInfringements, - )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. - .

    `; } - // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs + // bold description for 'not submitting a weekly summary' and logged hrs let emailDescription = requestForTimeOffEmailBody; if (!requestForTimeOffEmailBody && infringement.description) { - const sentences = infringement.description.split('.'); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - emailDescription = sentences.join('.'); - emailDescription = emailDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes('System auto-assigned infringement for editing your time entries') - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - emailDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace(/(not submitting a weekly summary)/gi, '$1'); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, + if (infringement.description.includes('not submitting a weekly summary')) { + emailDescription = infringement.description.replace( + /(not submitting a weekly summary)/gi, '$1', ); - emailDescription = sentences.join('.'); - emailDescription = emailDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); + emailDescription = emailDescription.replace(/(\d+\.\d{2})\s*hours/i, '$1 hours'); } else { emailDescription = `${infringement.description}`; } @@ -196,12 +133,13 @@ const userHelper = function () { // add administrative content const text = `Dear ${firstName} ${lastName},

    Oops, it looks like something happened and you’ve managed to get a blue square.

    -

    Date Assigned: ${moment(infringement.date).format('M-D-YYYY')}

    \ +

    Date Assigned: ${infringement.date}

    \

    Description: ${emailDescription}

    - ${descrInfringement} +

    Total Infringements: This is your ${moment + .localeData() + .ordinal(totalInfringements)} blue square of 5.

    ${finalParagraph} -

    Thank you,

    -

    One Community

    +

    Thank you, One Community

           
    @@ -227,7 +165,7 @@ const userHelper = function () { */ const emailWeeklySummariesForAllUsers = async (weekIndex = 1) => { const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - /* eslint-disable no-undef */ + logger.logInfo( `Job for emailing all users' weekly summaries starting at ${currentFormattedDate}`, ); @@ -239,7 +177,7 @@ const userHelper = function () { const results = await reportHelper.weeklySummaries(weekIndex, weekIndex); // checks for userProfiles who are eligible to receive the weeklySummary Reports await userProfile - .find({ getWeeklyReport: true }, { email: 1, teamCode: 1, _id: 0 }) + .find({ getWeeklyReport: true }, { email: 1, _id: 0 }) // eslint-disable-next-line no-shadow .then((results) => { mappedResults = results.map((ele) => ele.email); @@ -271,7 +209,6 @@ const userHelper = function () { weeklySummariesCount, weeklycommittedHours, weeklySummaryOption, - teamCode, } = result; if (email !== undefined && email !== null) { @@ -284,7 +221,7 @@ const userHelper = function () { const hoursLogged = result.totalSeconds[0] / 3600 || 0; const mediaUrlLink = mediaUrl ? `${mediaUrl}` : 'Not provided!'; - const teamCodeStr = teamCode ? `${teamCode}` : 'X-XXX'; + const googleDocLinkValue = adminLinks?.length > 0 ? adminLinks.find((link) => link.Name === 'Google Doc' && link.Link) @@ -334,9 +271,6 @@ const userHelper = function () { \n
    Name: ${firstName} ${lastName} -

    - Team Code: ${teamCodeStr || 'X-XXX'} -

    @@ -436,7 +370,6 @@ const userHelper = function () { */ const assignBlueSquareForTimeNotMet = async () => { try { - console.log('run'); const currentFormattedDate = moment().tz('America/Los_Angeles').format(); moment.tz('America/Los_Angeles').startOf('day').toISOString(); @@ -512,22 +445,16 @@ const userHelper = function () { * Condition: * 1. Not Started: Start Date > end date of last week && totalTangibleHrs === 0 && totalIntangibleHrs === 0 * 2. Short Week: Start Date (First time entrie) is after Monday && totalTangibleHrs === 0 && totalIntangibleHrs === 0 - * 3. No hours logged, and the account was after the start of last week. + * 3. No hour logged * * Notes: - * 1. Start date is automatically updated upon first time-log. + * 1. Start date is automatically updated upon frist time-log. * 2. User meet above condition but meet minimum hours without submitting weekly summary * should get a blue square as reminder. * */ let isNewUser = false; const userStartDate = moment(person.startDate); - if ( - person.totalTangibleHrs === 0 && - person.totalIntangibleHrs === 0 && - timeSpent === 0 && - userStartDate.isAfter(pdtStartOfLastWeek) - ) { - console.log('1'); + if (person.totalTangibleHrs === 0 && person.totalIntangibleHrs === 0 && timeSpent === 0) { isNewUser = true; } @@ -537,7 +464,6 @@ const userHelper = function () { userStartDate.isBefore(pdtEndOfLastWeek) && timeUtils.getDayOfWeekStringFromUTC(person.startDate) > 1) ) { - console.log('2'); isNewUser = true; } @@ -595,68 +521,33 @@ const userHelper = function () { historyInfringements = oldInfringements .map((item, index) => { let enhancedDescription; - if (item.description) { - let sentences = item.description.split('.'); - const dateRegex = - /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; - sentences = sentences.map((sentence) => - sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { - const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; - }), + if ( + item.description && + !item.description.includes('System auto-assigned infringement') + ) { + enhancedDescription = `${item.description}`; + } else if (item.description) { + // highlight not submitting a weekly summary and logged hrs + const sentences = item.description.split(/\.(?!\d)/); + sentences[0] = `${sentences[0]}`; + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = enhancedDescription.replace( + /(\d+\.\d{2})\s*hours/i, + '$1 hours', ); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes( - 'System auto-assigned infringement for editing your time entries', - ) - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - enhancedDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - enhancedDescription = `${item.description}`; - } } - return `

    ${index + 1}. Date: ${moment( - item.date, - ).format('M-D-YYYY')}, Description: ${enhancedDescription}

    `; + return `

    ${index + 1}. Date: ${item.date}, Description: ${enhancedDescription}

    `; }) .join(''); } // No extra hours is needed if blue squares isn't over 5. // length +1 is because new infringement hasn't been created at this stage. - const coreTeamExtraHour = Math.max(0, oldInfringements.length - 5); + const coreTeamExtraHour = Math.max(0, oldInfringements.length + 1 - 5); + const utcStartMoment = moment(pdtStartOfLastWeek).add(1, 'second'); const utcEndMoment = moment(pdtEndOfLastWeek).subtract(1, 'day').subtract(1, 'second'); @@ -677,10 +568,10 @@ const userHelper = function () { // eslint-disable-next-line prefer-destructuring requestForTimeOff = requestsForTimeOff[0]; requestForTimeOffStartingDate = moment(requestForTimeOff.startingDate).format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', ); requestForTimeOffEndingDate = moment(requestForTimeOff.endingDate).format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', ); requestForTimeOffreason = requestForTimeOff.reason; requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; @@ -692,9 +583,9 @@ const userHelper = function () { } else if (timeNotMet && !hasWeeklySummary) { if (person.role === 'Core Team') { description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. In the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', )} and ending ${pdtEndOfLastWeek.format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ person.weeklycommittedHours } hours + ${ @@ -710,15 +601,15 @@ const userHelper = function () { description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. For the hours portion, you logged ${timeSpent.toFixed( 2, )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + 'dddd YYYY-MM-DD', + )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; } } else if (timeNotMet) { if (person.role === 'Core Team') { description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. In the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', )} and ending ${pdtEndOfLastWeek.format( - 'dddd M-D-YYYY', + 'dddd YYYY-MM-DD', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ user.weeklycommittedHours } hours + ${ @@ -734,13 +625,13 @@ const userHelper = function () { description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. You logged ${timeSpent.toFixed( 2, )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + 'dddd YYYY-MM-DD', + )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; } } else { description = `System auto-assigned infringement for not submitting a weekly summary for the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + 'dddd YYYY-MM-DD', + )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; } const infringement = { @@ -765,7 +656,7 @@ const userHelper = function () { { new: true }, ); const administrativeContent = { - startDate: moment(person.startDate).utc().format('M-D-YYYY'), + startDate: moment(person.startDate).utc().format('YYYY-MM-DD'), role: person.role, userTitle: person.jobTitle[0], historyInfringements, @@ -780,7 +671,6 @@ const userHelper = function () { coreTeamExtraHour, requestForTimeOffEmailBody, administrativeContent, - weeklycommittedHours, ); } else { emailBody = getInfringementEmailBody( @@ -794,27 +684,11 @@ const userHelper = function () { administrativeContent, ); } - - let emailsBCCs; - /* eslint-disable array-callback-return */ - const blueSquareBCCs = await BlueSquareEmailAssignment.find() - .populate('assignedTo') - .exec(); - if (blueSquareBCCs.length > 0) { - emailsBCCs = blueSquareBCCs.map((assignment) => { - if (assignment.assignedTo.isActive === true) { - return assignment.email; - } - }); - } else { - emailsBCCs = null; - } - emailSender( status.email, 'New Infringement Assigned', emailBody, - emailsBCCs, + null, 'onecommunityglobal@gmail.com', status.email, null, @@ -1024,8 +898,7 @@ const userHelper = function () { }, ); - logger.logInfo(`Job deleting blue squares older than 1 year finished - at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); + logger.logInfo(results); } catch (err) { logger.logException(err); } @@ -1108,66 +981,30 @@ const userHelper = function () { if (original.length) { historyInfringements = original .map((item, index) => { - let enhancedDescription; - if (item.description) { - let sentences = item.description.split('.'); - const dateRegex = - /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; - sentences = sentences.map((sentence) => - sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { - const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; - }), + let enhancedDescription = item.description; + // highlight previous assigned reason manually + if (item.description && !item.description.includes('System auto-assigned infringement')) { + enhancedDescription = `${item.description}`; + } else { + // highlight not submitting a weekly summary and logged hrs + const sentences = item.description.split(/\.(?!\d)/); + sentences[0] = `${sentences[0]}`; + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = enhancedDescription.replace( + /(\d+\.\d{2})\s*hours/i, + '$1 hours', ); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes( - 'System auto-assigned infringement for editing your time entries', - ) - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - enhancedDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - enhancedDescription = `${item.description}`; - } } - return `

    ${index + 1}. Date: ${moment(item.date).format('M-D-YYYY')}, Description: ${enhancedDescription}

    `; + return `

    ${index + 1}. Date: ${item.date}, Description: ${enhancedDescription}

    `; }) .join(''); } const administrativeContent = { - startDate: moment(startDate).utc().format('M-D-YYYY'), + startDate: moment(startDate).utc().format('YYYY-MM-DD'), role, userTitle: jobTitle, historyInfringements, @@ -1258,7 +1095,7 @@ const userHelper = function () { personId, { $pull: { - badgeCollection: { badge: mongoose.Types.ObjectId(badgeId) }, + badgeCollection: { _id: mongoose.Types.ObjectId(badgeId) }, }, }, { new: true }, @@ -1329,7 +1166,7 @@ const userHelper = function () { const removePrevHrBadge = async function (personId, user, badgeCollection, hrs, weeks) { // Check each Streak Greater than One to check if it works - if (weeks < 2) { + if (weeks < 3) { return; } let removed = false; @@ -1338,7 +1175,7 @@ const userHelper = function () { { $match: { type: 'X Hours for X Week Streak', - weeks: { $gt: 0, $lt: weeks }, + weeks: { $gt: 1, $lt: weeks }, totalHrs: hrs, }, }, @@ -1359,13 +1196,13 @@ const userHelper = function () { if ( badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && badgeCollection[i].badge?.weeks === bdge.weeks && - badgeCollection[i].badge?.totalHrs === hrs && + bdge.hrs === hrs && !removed ) { changeBadgeCount( personId, badgeCollection[i].badge._id, - badgeCollection[i].count - 1, + badgeCollection[i].badge.count - 1, ); removed = true; return false; @@ -1496,48 +1333,6 @@ const userHelper = function () { }); }; - const getAllWeeksData = async (personId, user) => { - const userId = mongoose.Types.ObjectId(personId); - const weeksData = []; - const currentDate = moment().tz('America/Los_Angeles'); - const startDate = moment(user.createdDate).tz('America/Los_Angeles'); - const numWeeks = Math.ceil(currentDate.diff(startDate, 'days') / 7); - - // iterate through weeks to get hours of each week - for (let week = 1; week <= numWeeks; week += 1) { - const pdtstart = startDate - .clone() - .add(week - 1, 'weeks') - .startOf('week') - .format('YYYY-MM-DD'); - const pdtend = startDate.clone().add(week, 'weeks').subtract(1, 'days').format('YYYY-MM-DD'); - try { - const results = await dashboardHelper.laborthisweek(userId, pdtstart, pdtend); - const { timeSpent_hrs: timeSpent } = results[0]; - weeksData.push(timeSpent); - } catch (error) { - console.error(error); - throw error; - } - } - return weeksData; - }; - - const getMaxHrs = async (personId, user) => { - const weeksdata = await getAllWeeksData(personId, user); - return Math.max(...weeksdata); - }; - - const updatePersonalMax = async (personId, user) => { - try { - const MaxHrs = await getMaxHrs(personId, user); - user.personalBestMaxHrs = MaxHrs; - await user.save(); - } catch (error) { - console.error(error); - } - }; - // 'Personal Max', const checkPersonalMax = async function (personId, user, badgeCollection) { let badgeOfType; @@ -1557,18 +1352,17 @@ const userHelper = function () { } } await badge.findOne({ type: 'Personal Max' }).then((results) => { - const currentDate = moment(moment().format('MM-DD-YYYY'), 'MM-DD-YYYY') - .tz('America/Los_Angeles') - .format('MMM-DD-YY'); if ( user.lastWeekTangibleHrs && - user.lastWeekTangibleHrs >= user.personalBestMaxHrs && - !badgeOfType.earnedDate.includes(currentDate) + user.lastWeekTangibleHrs >= 1 && + user.lastWeekTangibleHrs === user.personalBestMaxHrs ) { if (badgeOfType) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType.badge._id)); - // Update the earnedDate array with the new date - badgeOfType.earnedDate.unshift(moment().format('MMM-DD-YYYY')); + changeBadgeCount( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + user.personalBestMaxHrs, + ); } else { addBadge(personId, mongoose.Types.ObjectId(results._id), user.personalBestMaxHrs); } @@ -1641,12 +1435,14 @@ const userHelper = function () { // 'X Hours for X Week Streak', const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { - let higherBadge = false; + // Handle Increasing the 1 week streak badges + await checkXHrsInOneWeek(personId, user, badgeCollection); + // Check each Streak Greater than One to check if it works await badge .aggregate([ { $match: { type: 'X Hours for X Week Streak', weeks: { $gt: 1 } } }, - // Group by 'week' property and sorting groups in descending order by 'week', then sorting badges within groups by 'totalHrs' in descending order. + { $sort: { weeks: -1, totalHrs: -1 } }, { $group: { _id: '$weeks', @@ -1655,41 +1451,6 @@ const userHelper = function () { }, }, }, - { - $project: { - _id: 1, - badges: { - $slice: [ - { - $map: { - input: '$badges', - in: { - _id: '$$this._id', - hrs: '$$this.hrs', - weeks: '$$this.weeks', - }, - }, - }, - { $size: '$badges' }, - ], - }, - }, - }, - { $unwind: '$badges' }, - { $sort: { _id: -1, 'badges.hrs': -1 } }, // Primary sort on _id, secondary sort on badges.hrs - { - $group: { - _id: '$_id', - badges: { - $push: { - _id: '$badges._id', - hrs: '$badges.hrs', - weeks: '$badges.weeks', - }, - }, - }, - }, - { $sort: { _id: -1 } }, // Add this $sort stage for the final sorting by _id ]) .then((results) => { let lastHr = -1; @@ -1726,7 +1487,6 @@ const userHelper = function () { } // if all checks for award badge are green double check that we havent already awarded a higher streak for the same number of hours if (awardBadge && bdge.hrs > lastHr) { - higherBadge = true; lastHr = bdge.hrs; if (badgeOfType && badgeOfType.totalHrs < bdge.hrs) { replaceBadge( @@ -1740,76 +1500,8 @@ const userHelper = function () { addBadge(personId, mongoose.Types.ObjectId(bdge._id)); removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); } else if (badgeOfType && badgeOfType.totalHrs === bdge.hrs) { - const lowerBound = badgeOfType.weeks; - let upperBound; - streak = 0; - - switch (bdge.weeks) { - case 2: - // In between 2Wk and 3Wk - upperBound = 3; - break; - case 3: - // In between 3Wk and 4Wk - upperBound = 4; - break; - case 4: - // In between 4Wk and 6Wk - upperBound = 6; - break; - case 6: - // In between 6Wk and 10Wk - upperBound = 10; - break; - case 10: - // In between 10Wk and 15Wk - upperBound = 15; - break; - case 15: - // In between 50Wk and 20Wk - upperBound = 20; - break; - case 20: - // In between 20Wk and 40Wk - upperBound = 40; - break; - case 40: - // In between 40Wk and 60Wk - upperBound = 60; - break; - case 60: - // In between 60Wk and 80Wk - upperBound = 80; - break; - case 80: - // In between 80Wk and 100Wk - upperBound = 100; - break; - case 100: - // In between 100Wk and 150Wk - upperBound = 150; - break; - case 150: - // In between 150Wk and 200Wk - upperBound = 200; - break; - default: - // Default case. Exiting function. - return; - } - for (let i = endOfArr; i >= endOfArr - upperBound + 1; i -= 1) { - if (user.savedTangibleHrs[i] >= bdge.hrs) { - streak += 1; - } - } - if (streak > lowerBound && streak < upperBound) { - higherBadge = false; - console.log('You are currently building an existing streak, no badge awarded.'); - } else { - console.log('You are currently building a new streak, new badge awarded'); - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); - } + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); + removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); } return false; } @@ -1818,9 +1510,6 @@ const userHelper = function () { }); }); }); - - // Handle Increasing the 1 week streak badges - if (!higherBadge) await checkXHrsInOneWeek(personId, user, badgeCollection); }; // 'Lead a team of X+' @@ -1988,7 +1677,6 @@ const userHelper = function () { const { _id, badgeCollection } = user; const personId = mongoose.Types.ObjectId(_id); - await updatePersonalMax(personId, user); await checkPersonalMax(personId, user, badgeCollection); await checkMostHrsWeek(personId, user, badgeCollection); await checkMinHoursMultiple(personId, user, badgeCollection); @@ -2030,30 +1718,8 @@ const userHelper = function () { }); }; - const sendDeactivateEmailBody = function (firstName, lastName, endDate, email, recipients) { - if (endDate) { - const subject = `IMPORTANT:${firstName} ${lastName} has been deactivated in the Highest Good Network`; - const emailBody = `

    Management,

    - -

    Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${endDate}. - Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

    - -

    With Gratitude,

    - -

    One Community

    `; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); - } - }; - const deActivateUser = async () => { try { - const emailReceivers = await userProfile.find( - { isActive: true, role: { $in: ['Owner'] } }, - '_id isActive role email', - ); - const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, '_id isActive endDate', @@ -2063,42 +1729,36 @@ const userHelper = function () { const { endDate } = user; endDate.setHours(endDate.getHours() + 7); if (moment().isAfter(moment(endDate).add(1, 'days'))) { - try { - await userProfile.findByIdAndUpdate( - user._id, - user.set({ - isActive: false, - }), - { new: true }, - ); - } catch (err) { - // Log the error and continue to the next user - logger.logException(err, `Error in deActivateUser. Failed to update User ${user._id}`); - continue; - } + await userProfile.findByIdAndUpdate( + user._id, + user.set({ + isActive: false, + }), + { new: true }, + ); const id = user._id; const person = await userProfile.findById(id); + const lastDay = moment(person.endDate).format('YYYY-MM-DD'); logger.logInfo(`User with id: ${user._id} was de-acticated at ${moment().format()}.`); - person.teams.map(async (teamId) => { - const managementEmails = await userHelper.getTeamManagementEmail(teamId); - if (Array.isArray(managementEmails) && managementEmails.length > 0) { - managementEmails.forEach((management) => { - recipients.push(management.email); - }); - } - }); - sendDeactivateEmailBody( - person.firstName, - person.lastName, - lastDay, - person.email, - recipients, - ); + + const subject = `IMPORTANT:${person.firstName} ${person.lastName} has been deactivated in the Highest Good Network`; + + const emailBody = `

    Hi Admin!

    + +

    This email is to let you know that ${person.firstName} ${person.lastName} has completed their scheduled last day (${lastDay}) and been deactivated in the Highest Good Network application.

    + +

    This is their email from the system: ${person.email}. Please email them to let them know their work is complete and thank them for their volunteer time with One Community.

    + +

    Thanks!

    + +

    The HGN A.I. (and One Community)

    `; + + emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); } } } catch (err) { - logger.logException(err, 'Unexpected error in deActivateUser'); + logger.logException(err); } }; @@ -2112,8 +1772,7 @@ const userHelper = function () { try { await token.deleteMany({ isCancelled: true, expiration: { $lt: ninetyDaysAgo } }); } catch (error) { - /* eslint-disable no-undef */ - logger.logException(error, `Error in deleteExpiredTokens. Date ${currentDate}`); + logger.logException(error); } }; @@ -2124,10 +1783,7 @@ const userHelper = function () { try { await timeOffRequest.deleteMany({ endingDate: { $lte: utcEndMoment } }); } catch (error) { - logger.logException( - error, - `Error deleting expired time-off requests: utcEndMoment ${utcEndMoment}`, - ); + console.error('Error deleting expired time off requests:', error); } }; @@ -2135,19 +1791,16 @@ const userHelper = function () { changeBadgeCount, getUserName, getTeamMembers, - getTeamManagementEmail, validateProfilePic, assignBlueSquareForTimeNotMet, applyMissedHourForCoreTeam, deleteBlueSquareAfterYear, reActivateUser, - sendDeactivateEmailBody, deActivateUser, notifyInfringements, getInfringementEmailBody, emailWeeklySummariesForAllUsers, awardNewBadges, - checkXHrsForXWeeks, getTangibleHoursReportedThisWeekByUserId, deleteExpiredTokens, deleteOldTimeOffRequests, diff --git a/src/models/BlueSquareEmailAssignment.js b/src/models/BlueSquareEmailAssignment.js deleted file mode 100644 index d59229e4b..000000000 --- a/src/models/BlueSquareEmailAssignment.js +++ /dev/null @@ -1,10 +0,0 @@ -const mongoose = require("mongoose"); - -const { Schema } = mongoose; - -const BlueSquareEmailAssignmentSchema = new Schema({ - email: { type: String, required: true, unique: true }, - assignedTo: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true } -}); - -module.exports = mongoose.model("BlueSquareEmailAssignment", BlueSquareEmailAssignmentSchema, "BlueSquareEmailAssignments"); \ No newline at end of file diff --git a/src/models/bmdashboard/buildingInventoryItem.js b/src/models/bmdashboard/buildingInventoryItem.js index 77fa2135a..ab8e904ed 100644 --- a/src/models/bmdashboard/buildingInventoryItem.js +++ b/src/models/bmdashboard/buildingInventoryItem.js @@ -44,8 +44,7 @@ const largeItemBaseSchema = mongoose.Schema({ // actual purchases (once there is a system) may need their own subdoc // subdoc may contain below purchaseStatus and rental fields // for now they have default dummy values - purchaseStatus: { type: String, enum: ['Rental', 'Purchase','Needed', 'Purchased'], default: 'Rental' }, - condition: { type: String, enum: ['Like New', 'Good', 'Worn', 'Lost', 'Needs Repair', 'Needs Replacing'], default: 'Like New'}, + purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], default: 'Rental' }, // TODO: rental fields should be required if purchaseStatus === "Rental" rentedOnDate: { type: Date, default: Date.now() }, rentalDueDate: { type: Date, default: new Date(Date.now() + (3600 * 1000 * 24 * 14)) }, @@ -74,7 +73,6 @@ const largeItemBaseSchema = mongoose.Schema({ responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, type: { type: String, enum: ['Check In', 'Check Out'] }, }], - userResponsible: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, // new field }); const largeItemBase = mongoose.model('largeItemBase', largeItemBaseSchema, 'buildingInventoryItems'); @@ -128,11 +126,6 @@ const buildingReusable = smallItemBase.discriminator('reusable_item', new mongoo const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schema({ // TODO: add function to create simple numeric code for on-site tool tracking code: { type: String, default: '001' }, - purchaseStatus: { - type: String, - enum: ['Rental', 'Purchased'], // Override enum values - default: 'Rental', -} })); //----------------- @@ -145,8 +138,8 @@ const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schem // ex: tractors, excavators, bulldozers const buildingEquipment = largeItemBase.discriminator('equipment_item', new mongoose.Schema({ - // isTracked: { type: Boolean, required: true }, // has asset tracker - // assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) + isTracked: { type: Boolean, required: true }, // has asset tracker + assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) })); module.exports = { diff --git a/src/models/bmdashboard/buildingInventoryType.js b/src/models/bmdashboard/buildingInventoryType.js index 7dcaa38dc..9173bf5cc 100644 --- a/src/models/bmdashboard/buildingInventoryType.js +++ b/src/models/bmdashboard/buildingInventoryType.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; + //--------------------------- // BASE INVENTORY TYPE SCHEMA //--------------------------- @@ -12,7 +13,7 @@ const invTypeBaseSchema = new Schema({ name: { type: String, required: true }, description: { type: String, required: true, maxLength: 150 }, imageUrl: String, - createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfiles' }, }); const invTypeBase = mongoose.model('invTypeBase', invTypeBaseSchema, 'buildingInventoryTypes'); @@ -58,23 +59,8 @@ const reusableType = invTypeBase.discriminator('reusable_type', new mongoose.Sch const toolType = invTypeBase.discriminator('tool_type', new mongoose.Schema({ category: { type: String, enum: ['Tool'] }, - invoice: String, - purchaseRental: String, - fromDate: Date, - toDate:Date, - condition: String, - phoneNumber: String, - quantity: Number, - currency: String, - unitPrice: Number, - shippingFee: Number, - taxes: Number, - totalPriceWithShipping: Number, - images: String, - link: String, - - // isPowered: { type: Boolean, required: true }, - // powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) + isPowered: { type: Boolean, required: true }, + powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) })); //--------------------------- diff --git a/src/models/project.js b/src/models/project.js index da9979628..6a78a0b31 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -5,25 +5,9 @@ const { Schema } = mongoose; const projectschema = new Schema({ projectName: { type: String, required: true, unique: true }, isActive: { type: Boolean, default: true }, - isArchived: { type: Boolean, default: false }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, - membersModifiedDatetime: { type: Date, default: Date.now() }, - category: { - type: String, - enum: [ - 'Food', - 'Energy', - 'Housing', - 'Education', - 'Society', - 'Economics', - 'Stewardship', - 'Other', - 'Unspecified', - ], - default: 'Other', - }, + category: { type: String, enum: ['Food', 'Energy', 'Housing', 'Education', 'Society', 'Economics', 'Stewardship', 'Other', 'Unspecified'], default: 'Other' }, }); module.exports = mongoose.model('project', projectschema, 'projects'); diff --git a/src/models/team.js b/src/models/team.js index a92e740b1..1c543827b 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -2,6 +2,12 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; +/** + * This schema represents a team in the system. + * + * Deprecated field: teamCode. Team code is no longer associated with a team. + * Team code is used as a text string identifier in the user profile data model. + */ const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, @@ -11,19 +17,19 @@ const team = new Schema({ { userId: { type: mongoose.SchemaTypes.ObjectId, required: true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, - visible: { type : 'Boolean', default:true}, }, ], + // Deprecated field teamCode: { type: 'String', default: '', validate: { validator(v) { - const teamCoderegex = /^(.{5,7}|^$)$/; + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; return teamCoderegex.test(v); }, message: - 'Please enter a code in the format of A-AAAA or AAAAA, with optional numbers, and a total length between 5 and 7 characters.', + 'Please enter a code in the format of A-AAA or AAAAA', }, }, }); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index ea5303b3a..4ae0b94fa 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -15,7 +15,6 @@ const TimeEntry = new Schema({ isTangible: { type: Boolean, default: false }, createdDateTime: { type: Date }, lastModifiedDateTime: { type: Date, default: Date.now }, - isActive: { type: Boolean, default: true }, }); module.exports = mongoose.model('timeEntry', TimeEntry, 'timeEntries'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index c3e42b778..f14ef63d3 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -55,7 +55,9 @@ const userProfileSchema = new Schema({ type: String, required: true, unique: true, - validate: [validate({ validator: 'isEmail', message: 'Email address is invalid' })], + validate: [ + validate({ validator: 'isEmail', message: 'Email address is invalid' }), + ], }, copiedAiPrompt: { type: Date, default: Date.now() }, emailSubscriptions: { @@ -75,7 +77,7 @@ const userProfileSchema = new Schema({ startDate: { type: Date, required: true, - default() { + default () { return this.createdDate; }, }, @@ -138,15 +140,6 @@ const userProfileSchema = new Schema({ country: { type: String, default: '' }, city: { type: String, default: '' }, }, - homeCountry: { - userProvided: { type: String, default: '' }, - coords: { - lat: { type: Number, default: '' }, - lng: { type: Number, default: '' }, - }, - country: { type: String, default: '' }, - city: { type: String, default: '' }, - }, oldInfringements: [ { date: { type: String, required: true }, @@ -226,21 +219,19 @@ const userProfileSchema = new Schema({ ], weeklySummaryNotReq: { type: Boolean, default: false }, timeZone: { type: String, required: true, default: 'America/Los_Angeles' }, - isVisible: { type: Boolean, default: true }, + isVisible: { type: Boolean, default: false }, weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, isFirstTimelog: { type: Boolean, default: true }, - badgeCount: { type: Number, default: 0 }, teamCode: { type: String, default: '', validate: { validator(v) { - const teamCoderegex = /^(.{5,7}|^$)$/; + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; return teamCoderegex.test(v); }, - message: - 'Please enter a code in the format of A-AAAA or AAAAA, with optional numbers, and a total length between 5 and 7 characters.', + message: 'Please enter a code in the format of A-AAA or AAAAA', }, }, infoCollections: [ @@ -271,4 +262,11 @@ userProfileSchema.pre('save', function (next) { .catch((error) => next(error)); }); -module.exports = mongoose.model('userProfile', userProfileSchema, 'userProfiles'); +userProfileSchema.index({ teamCode: 1 }); +userProfileSchema.index({ email: 1 }); + +module.exports = mongoose.model( + 'userProfile', + userProfileSchema, + 'userProfiles', +); diff --git a/src/models/wbs.js b/src/models/wbs.js index bcfbab074..73f9fd413 100644 --- a/src/models/wbs.js +++ b/src/models/wbs.js @@ -3,11 +3,13 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const wbsschema = new Schema({ + wbsName: { type: String, required: true }, projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'project' }, isActive: { type: Boolean, default: true }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, + }); module.exports = mongoose.model('wbs', wbsschema, 'wbs'); diff --git a/src/routes/BlueSquareEmailAssignmentRouter.js b/src/routes/BlueSquareEmailAssignmentRouter.js deleted file mode 100644 index 0384bb4f8..000000000 --- a/src/routes/BlueSquareEmailAssignmentRouter.js +++ /dev/null @@ -1,18 +0,0 @@ -const express = require('express'); - -const routes = function (BlueSquareEmailAssignment,userProfile) { - const BlueSquareEmailAssignmentRouter = express.Router(); - const controller = require('../controllers/BlueSquareEmailAssignmentController')(BlueSquareEmailAssignment,userProfile); - - BlueSquareEmailAssignmentRouter.route('/AssignBlueSquareEmail') - .get(controller.getBlueSquareEmailAssignment) - .post(controller.setBlueSquareEmailAssignment) - - BlueSquareEmailAssignmentRouter.route('/AssignBlueSquareEmail/:id') - .delete(controller.deleteBlueSquareEmailAssignment); - - - return BlueSquareEmailAssignmentRouter; -}; - -module.exports = routes; \ No newline at end of file diff --git a/src/routes/badgeRouter.js b/src/routes/badgeRouter.js index d839813b8..3f3c8b892 100644 --- a/src/routes/badgeRouter.js +++ b/src/routes/badgeRouter.js @@ -4,18 +4,17 @@ const routes = function (badge) { const controller = require('../controllers/badgeController')(badge); const badgeRouter = express.Router(); + + badgeRouter.route('/badge') + .get(controller.getAllBadges) + .post(controller.postBadge); - // badgeRouter.get('/badge/awardBadgesTest', controller.awardBadgesTest); + badgeRouter.route('/badge/:badgeId') + .delete(controller.deleteBadge) + .put(controller.putBadge); - badgeRouter.route('/badge').get(controller.getAllBadges).post(controller.postBadge); - - badgeRouter.route('/badge/:badgeId').delete(controller.deleteBadge).put(controller.putBadge); - - badgeRouter.route('/badge/assign/:userId').put(controller.assignBadges); - - badgeRouter.route('/badge/badgecount/:userId').get(controller.getBadgeCount).put(controller.putBadgecount); - - badgeRouter.route('/badge/badgecount/reset/:userId').put(controller.resetBadgecount); + badgeRouter.route('/badge/assign/:userId') + .put(controller.assignBadges); return badgeRouter; }; diff --git a/src/routes/bmdashboard/bmEquipmentRouter.js b/src/routes/bmdashboard/bmEquipmentRouter.js index e97d92cf1..111d50f77 100644 --- a/src/routes/bmdashboard/bmEquipmentRouter.js +++ b/src/routes/bmdashboard/bmEquipmentRouter.js @@ -10,8 +10,6 @@ const routes = function (BuildingEquipment) { equipmentRouter.route('/equipment/purchase').post(controller.bmPurchaseEquipments); - equipmentRouter.route('/equipments').get(controller.fetchBMEquipments); - return equipmentRouter; }; diff --git a/src/routes/bmdashboard/bmInventoryTypeRouter.js b/src/routes/bmdashboard/bmInventoryTypeRouter.js index 2f57105e5..3d940ac61 100644 --- a/src/routes/bmdashboard/bmInventoryTypeRouter.js +++ b/src/routes/bmdashboard/bmInventoryTypeRouter.js @@ -20,14 +20,10 @@ const routes = function (baseInvType, matType, consType, reusType, toolType, equ inventoryTypeRouter.route('/consumables').post(controller.addConsumableType); - inventoryTypeRouter.route('/tools').post(controller.addToolType); - inventoryTypeRouter.route('/invtypes/tools').get(controller.fetchToolTypes); inventoryTypeRouter.route('/invtypes/equipment').post(controller.addEquipmentType); - inventoryTypeRouter.route('/invtypes/equipments').get(controller.fetchEquipmentTypes); - inventoryTypeRouter.route('/invtypes/consumables').get(controller.fetchConsumableTypes); // Route for fetching types by selected type diff --git a/src/routes/bmdashboard/bmReusableRouter.js b/src/routes/bmdashboard/bmReusableRouter.js index 2a8c7af52..a6235823c 100644 --- a/src/routes/bmdashboard/bmReusableRouter.js +++ b/src/routes/bmdashboard/bmReusableRouter.js @@ -10,12 +10,6 @@ const routes = function (BuildingReusable) { BuildingReusableController.route('/reusables/purchase') .post(controller.purchaseReusable); - BuildingReusableController.route('/updateReusableRecord') - .post(controller.bmPostReusableUpdateRecord); - - BuildingReusableController.route('/updateReusableRecordBulk') - .post(controller.bmPostReusableUpdateBulk); - return BuildingReusableController; }; diff --git a/src/routes/bmdashboard/bmToolRouter.js b/src/routes/bmdashboard/bmToolRouter.js index 5a9a96e78..e58895433 100644 --- a/src/routes/bmdashboard/bmToolRouter.js +++ b/src/routes/bmdashboard/bmToolRouter.js @@ -1,11 +1,8 @@ const express = require('express'); -const routes = function (BuildingTool, ToolType) { +const routes = function (BuildingTool) { const toolRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool, ToolType); - - toolRouter.route('/tools') - .get(controller.fetchAllTools); + const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool); toolRouter.route('/tools/:toolId') .get(controller.fetchSingleTool); @@ -13,9 +10,6 @@ const routes = function (BuildingTool, ToolType) { toolRouter.route('/tools/purchase') .post(controller.bmPurchaseTools); - toolRouter.route('/tools/log') - .post(controller.bmLogTools); - return toolRouter; }; diff --git a/src/routes/forgotPwdRouter.test.js b/src/routes/forgotPwdRouter.test.js deleted file mode 100644 index ca434433a..000000000 --- a/src/routes/forgotPwdRouter.test.js +++ /dev/null @@ -1,83 +0,0 @@ -const request = require('supertest'); -const { jwtPayload } = require('../test'); -const { app } = require('../app'); -const { - mockReq, - mockUser, - mongoHelper: { dbConnect, dbDisconnect, dbClearCollections }, - createTestPermissions, - createUser, -} = require('../test'); - -const agent = request.agent(app); - -describe('forgotPwd routes', () => { - let user; - let token; - const reqBody = { - // This is the user we want to create - body: { - ...mockReq.body, - ...mockUser(), - }, - }; - - beforeAll(async () => { - await dbConnect(); - await createTestPermissions(); - user = await createUser(); // This is the requestor user - token = jwtPayload(user); - }); - - beforeEach(async () => { - await dbClearCollections('userProfiles'); - }); - - afterAll(async () => { - await dbClearCollections('userProfiles'); - await dbDisconnect(); - }); - - describe('API routes', () => { - it("should return 404 if route doesn't exists", async () => { - await agent - .post('/api/forgotpasswords') - .send(reqBody.body) - .set('Authorization', token) - .expect(404); - }); - }); - - describe('postForgotPassword', () => { - test('Should return 400 when using findOne for user who does not exists in database', async () => { - // finds user data of a user who does not exists in database - const response = await agent - .post('/api/forgotpassword') - .send(reqBody.body) - .set('Authorization', token) - .expect(400); - - expect(response.body.error).toBe('No Valid user was found'); - }); - - test('Should return 200 when successfully generated a temp password for user', async () => { - // adding a user to the database - let response = await agent - .post('/api/userProfile') - .send(reqBody.body) - .set('Authorization', token) - .expect(200); - - expect(response.body).toBeTruthy(); - - // finds user data of a user who exists in database - response = await agent - .post('/api/forgotpassword') - .send(reqBody.body) - .set('Authorization', token) - .expect(200); - - expect(response.body.message).toBe('generated new password'); - }); - }); -}); diff --git a/src/routes/mapLocationsRouter.test.js b/src/routes/mapLocationsRouter.test.js new file mode 100644 index 000000000..5eb9a87d1 --- /dev/null +++ b/src/routes/mapLocationsRouter.test.js @@ -0,0 +1,200 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + createUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearAll }, +} = require('../test'); +const MapLocation = require('../models/mapLocation'); + +const agent = request.agent(app); + +describe('mapLocations routes', () => { + let ownerUser; + let volunteerUser; + let ownerToken; + let volunteerToken; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + ownerUser = await createUser(); + volunteerUser = await createUser(); + ownerUser.role = 'Owner'; + volunteerUser.role = 'Volunteer'; + ownerToken = jwtPayload(ownerUser); + volunteerToken = jwtPayload(volunteerUser); + reqBody = { + ...reqBody, + firstName: volunteerUser.firstName, + lastName: volunteerUser.lastName, + jobTitle: 'Software Engineer', + location: { + userProvided: 'A', + coords: { + lat: '51', + lng: '110', + }, + country: 'Test', + city: 'Usa', + }, + _id: volunteerUser._id, + type: 'user', + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('mapLocationRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.get('/api/mapLocations').send(reqBody).expect(401); + await agent.put('/api/mapLocations').send(reqBody).expect(401); + await agent.patch('/api/mapLocations').send(reqBody).expect(401); + await agent.delete('/api/mapLocations/123').send(reqBody).expect(401); + }); + + it('should return 404 if the route does not exist', async () => { + await agent + .get('/api/mapLocation') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .put('/api/mapLocation') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .patch('/api/mapLocation') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .delete('/api/mapLocation/123') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + }); + }); + + describe('getMapLocation routes', () => { + it('Should return 200 and the users on success', async () => { + const expected = { + mUsers: [], + users: [ + { + location: { + city: '', + coords: { + lat: 51, + lng: 110, + }, + country: '', + userProvided: '', + }, + isActive: ownerUser.isActive, + jobTitle: ownerUser.jobTitle[0], + _id: ownerUser._id.toString(), + firstName: ownerUser.firstName, + lastName: ownerUser.lastName, + }, + { + location: { + city: '', + coords: { + lat: 51, + lng: 110, + }, + country: '', + userProvided: '', + }, + isActive: volunteerUser.isActive, + jobTitle: volunteerUser.jobTitle[0], + _id: volunteerUser._id.toString(), + firstName: volunteerUser.firstName, + lastName: volunteerUser.lastName, + }, + ], + }; + + const response = await agent + .get('/api/mapLocations') + .set('Authorization', ownerToken) + .send(reqBody) + .expect(200); + + expect(response.body).toEqual(expected); + }); + }); + + describe('putMapLocation route', () => { + it('Should return 200 on success', async () => { + const response = await agent + .put('/api/mapLocations') + .set('Authorization', ownerToken) + .send(reqBody) + .expect(200); + + const expected = { + _id: expect.anything(), + __v: expect.anything(), + firstName: reqBody.firstName, + lastName: reqBody.lastName, + jobTitle: reqBody.jobTitle, + location: reqBody.location, + isActive: false, + title: 'Prior to HGN Data Collection', + }; + + expect(response.body).toEqual(expected); + }); + }); + + describe('patchMapLocation route', () => { + it('Should return 200 on success', async () => { + reqBody.location.coords.lat = 51; + reqBody.location.coords.lng = 110; + const res = await agent + .patch('/api/mapLocations') + .set('Authorization', ownerToken) + .send(reqBody) + .expect(200); + + const expected = { + firstName: reqBody.firstName, + lastName: reqBody.lastName, + jobTitle: [reqBody.jobTitle], + location: reqBody.location, + _id: reqBody._id.toString(), + type: reqBody.type, + }; + + expect(res.body).toEqual(expected); + }); + }); + + describe('Delete map locations route', () => { + it('Should return 200 on success', async () => { + const _map = new MapLocation(); + _map.firstName = reqBody.firstName; + _map.lastName = reqBody.lastName; + _map.location = reqBody.location; + _map.jobTitle = reqBody.jobTitle; + + const map = await _map.save(); + + const res = await agent + .delete(`/api/mapLocations/${map._id}`) + .set('Authorization', ownerToken) + .send(reqBody); + + expect(res.body).toEqual({ message: 'The location was successfully removed!' }); + }); + }); +}); diff --git a/src/routes/mouseoverTextRouter.test.js b/src/routes/mouseoverTextRouter.test.js deleted file mode 100644 index bbdebf70c..000000000 --- a/src/routes/mouseoverTextRouter.test.js +++ /dev/null @@ -1,98 +0,0 @@ -const request = require('supertest'); -const { jwtPayload } = require('../test'); -const { app } = require('../app'); -const { - mockReq, - createUser, - mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, -} = require('../test'); -const MouseoverText = require('../models/mouseoverText'); - -const agent = request.agent(app); - -describe('mouseoverText routes', () => { - let adminUser; - let adminToken; - let reqBody = { - ...mockReq.body, - }; - - beforeAll(async () => { - await dbConnect(); - adminUser = await createUser(); - adminToken = jwtPayload(adminUser); - }); - - beforeEach(async () => { - await dbClearCollections('mouseoverText'); - reqBody = { - ...reqBody, - newMouseoverText: 'new mouseoverText', - }; - }); - - afterAll(async () => { - await dbClearAll(); - await dbDisconnect(); - }); - - describe('mouseoverTextRoutes', () => { - it('should return 401 if authorization header is not present', async () => { - await agent.post('/api/mouseoverText').send(reqBody).expect(401); - await agent.get('/api/mouseoverText').send(reqBody).expect(401); - await agent.put(`/api/mouseoverText/randomId`).send(reqBody).expect(401); - }); - }); - describe('createMouseoverText route', () => { - it('Should return 201 if create new mouseoverText successfully', async () => { - const response = await agent - .post('/api/mouseoverText') - .send(reqBody) - .set('Authorization', adminToken) - .expect(201); - - expect(response.body).toEqual({ - mouseoverText: { - _id: expect.anything(), - __v: expect.anything(), - mouseoverText: reqBody.newMouseoverText, - }, - _serverMessage: 'MouseoverText succesfuly created!', - }); - }); - }); - describe('getMouseoverText route', () => { - it('Should return 201 if create new mouseoverText successfully', async () => { - const _mouseoverText = new MouseoverText(); - _mouseoverText.mouseoverText = 'sample mouseoverText'; - await _mouseoverText.save(); - await agent.get('/api/mouseoverText').set('Authorization', adminToken).expect(200); - }); - }); - describe('updateMouseoverText route', () => { - it('Should return 500 if any error in finding mouseoverText by Id', async () => { - reqBody.newMouseoverText = null; - const response = await agent - .put('/api/mouseoverText/randomId') - .send(reqBody) - .set('Authorization', adminToken) - .expect(500); - expect(response.text).toEqual('MouseoverText not found with the given ID'); - }); - it('Should return 201 if updating mouseoverText successfully', async () => { - const _mouseoverText = new MouseoverText(); - _mouseoverText.mouseoverText = 'sample mouseoverText'; - const mouseoverText = await _mouseoverText.save(); - const response = await agent - .put(`/api/mouseoverText/${mouseoverText._id}`) - .send(reqBody) - .set('Authorization', adminToken) - .expect(201); - expect(response.body).toEqual({ - _id: expect.anything(), - __v: expect.anything(), - mouseoverText: reqBody.newMouseoverText, - }); - }); - }); -}); diff --git a/src/routes/reportsRouter.js b/src/routes/reportsRouter.js index a80295ded..7a98fca8b 100644 --- a/src/routes/reportsRouter.js +++ b/src/routes/reportsRouter.js @@ -1,39 +1,23 @@ /* eslint-disable quotes */ -const express = require('express'); +const express = require("express"); const route = function () { - const controller = require('../controllers/reportsController')(); + const controller = require("../controllers/reportsController")(); const reportsRouter = express.Router(); reportsRouter - .route('/reports/recepients/:userid') + .route("/reports/recepients/:userid") .patch(controller.saveReportsRecepients) .delete(controller.deleteReportsRecepients); - reportsRouter.route('/reports/getrecepients').get(controller.getReportRecipients); - - reportsRouter.route('/reports/weeklysummaries').get(controller.getWeeklySummaries); - - reportsRouter - .route('/reports/overviewsummaries/volunteerstats') - .get(controller.getVolunteerStats); - reportsRouter - .route('/reports/overviewsummaries/volunteerhoursstats') - .get(controller.getVolunteerHoursStats); + .route("/reports/getrecepients") + .get(controller.getReportRecipients); reportsRouter - .route('/reports/overviewsummaries/taskandprojectstats') - .get(controller.getTaskAndProjectStats); - - reportsRouter - .route('/reports/overviewsummaries/volunteerrolestats') - .get(controller.getVolunteerRoleStats); - - reportsRouter.route('/reports/overviewsummaries/bluestats').get(controller.getBlueSquareStats); - - reportsRouter.route('/reports/volunteerstats').get(controller.getVolunteerStatsData); + .route("/reports/weeklysummaries") + .get(controller.getWeeklySummaries); return reportsRouter; }; diff --git a/src/routes/rolePresetRouter.test.js b/src/routes/rolePresetRouter.test.js deleted file mode 100644 index 31199be96..000000000 --- a/src/routes/rolePresetRouter.test.js +++ /dev/null @@ -1,238 +0,0 @@ -const request = require('supertest'); -const { jwtPayload } = require('../test'); -const { app } = require('../app'); -const { - mockReq, - createUser, - createRole, - mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, -} = require('../test'); -const RolePreset = require('../models/rolePreset'); - -const agent = request.agent(app); - -describe('rolePreset routes', () => { - let adminUser; - let adminToken; - let volunteerUser; - let volunteerToken; - let reqBody = { - ...mockReq.body, - }; - - beforeAll(async () => { - await dbConnect(); - adminUser = await createUser(); - volunteerUser = await createUser(); - volunteerUser.role = 'Volunteer'; - adminToken = jwtPayload(adminUser); - volunteerToken = jwtPayload(volunteerUser); - // create 2 roles. One with permission and one without - await createRole('Administrator', ['putRole']); - await createRole('Volunteer', []); - }); - - beforeEach(async () => { - await dbClearCollections('rolePreset'); - reqBody = { - ...reqBody, - roleName: 'some roleName', - presetName: 'some Preset', - permissions: ['test', 'write'], - }; - }); - - afterAll(async () => { - await dbClearAll(); - await dbDisconnect(); - }); - - describe('rolePresetRoutes', () => { - it('should return 401 if authorization header is not present', async () => { - await agent.post('/api/rolePreset').send(reqBody).expect(401); - await agent.get('/api/rolePreset/randomRoleName').send(reqBody).expect(401); - await agent.put(`/api/rolePreset/randomId`).send(reqBody).expect(401); - await agent.delete('/api/rolePreser/randomId').send(reqBody).expect(401); - }); - }); - - describe('Post rolePreset route', () => { - it('Should return 403 if user does not have permissions', async () => { - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', volunteerToken) - .expect(403); - expect(response.text).toEqual('You are not authorized to make changes to roles.'); - }); - - it('Should return 400 if missing roleName', async () => { - reqBody.roleName = null; - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', adminToken) - .expect(400); - - expect(response.body).toEqual({ - error: 'roleName, presetName, and permissions are mandatory fields.', - }); - }); - - it('Should return 400 if missing presetName', async () => { - reqBody.presetName = null; - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', adminToken) - .expect(400); - - expect(response.body).toEqual({ - error: 'roleName, presetName, and permissions are mandatory fields.', - }); - }); - - it('Should return 400 if missing permissions', async () => { - reqBody.permissions = null; - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', adminToken) - .expect(400); - - expect(response.body).toEqual({ - error: 'roleName, presetName, and permissions are mandatory fields.', - }); - }); - - it('Should return 201 if the rolePreset is successfully created', async () => { - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', adminToken) - .expect(201); - - expect(response.body).toEqual({ - newPreset: { - _id: expect.anything(), - __v: expect.anything(), - roleName: reqBody.roleName, - presetName: reqBody.presetName, - permissions: reqBody.permissions, - }, - message: 'New preset created', - }); - }); - }); - - describe('get Presets ByRole route', () => { - it('Should return 403 if user does not have permissions', async () => { - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', volunteerToken) - .expect(403); - - expect(response.text).toEqual('You are not authorized to make changes to roles.'); - }); - - it('Should return 200 if getPreset By role successfully', async () => { - const _rolePreset = new RolePreset(); - _rolePreset.roleName = 'sample roleName'; - _rolePreset.presetName = 'sample presetName'; - _rolePreset.permissions = ['sample permissions']; - const rolePreset = await _rolePreset.save(); - const response = await agent - .get(`/api/rolePreset/${rolePreset.roleName}`) - .set('Authorization', adminToken) - .expect(200); - - expect(response.body).toEqual([ - { - _id: expect.anything(), - __v: expect.anything(), - roleName: rolePreset.roleName, - presetName: rolePreset.presetName, - permissions: expect.arrayContaining(rolePreset.permissions), - }, - ]); - }); - }); - describe('update Preset route', () => { - it('Should return 403 if user does not have permissions', async () => { - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', volunteerToken) - .expect(403); - - expect(response.text).toEqual('You are not authorized to make changes to roles.'); - }); - - it('Should return 400 if the route does not exist', async () => { - await agent - .put('/api/rolePreset/randomId123') - .send(reqBody) - .set('Authorization', adminToken) - .expect(400); - }); - - it('Should return 200 if update Preset By Id successfully', async () => { - const _rolePreset = new RolePreset(); - _rolePreset.roleName = reqBody.roleName; - _rolePreset.presetName = reqBody.presetName; - _rolePreset.permissions = reqBody.permissions; - const rolePreset = await _rolePreset.save(); - const response = await agent - .put(`/api/rolePreset/${rolePreset._id}`) - .send(reqBody) - .set('Authorization', adminToken) - .expect(200); - - expect(response.body).toEqual({ - _id: expect.anything(), - __v: expect.anything(), - roleName: reqBody.roleName, - presetName: reqBody.presetName, - permissions: expect.arrayContaining(reqBody.permissions), - }); - }); - }); - describe('delete Preset route', () => { - it('Should return 403 if user does not have permissions', async () => { - const response = await agent - .post('/api/rolePreset') - .send(reqBody) - .set('Authorization', volunteerToken) - .expect(403); - - expect(response.text).toEqual('You are not authorized to make changes to roles.'); - }); - - it('Should return 400 if the route does not exist', async () => { - await agent - .delete('/api/rolePreset/randomId123') - .send(reqBody) - .set('Authorization', adminToken) - .expect(400); - }); - - it('Should return 200 if update Preset By Id successfully', async () => { - const _rolePreset = new RolePreset(); - _rolePreset.roleName = reqBody.roleName; - _rolePreset.presetName = reqBody.presetName; - _rolePreset.permissions = reqBody.permissions; - const rolePreset = await _rolePreset.save(); - - const response = await agent - .delete(`/api/rolePreset/${rolePreset._id}`) - .send(reqBody) - .set('Authorization', adminToken) - .expect(200); - - expect(response.body).toEqual({ - message: 'Deleted preset', - }); - }); - }); -}); diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index 4f91dc4b2..b404cea0a 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -2,42 +2,56 @@ const express = require('express'); const routes = function (task, userProfile) { const controller = require('../controllers/taskController')(task, userProfile); - const taskRouter = express.Router(); + const wbsRouter = express.Router(); - taskRouter - .route('/tasks/:wbsId/:level/:mother') + wbsRouter.route('/tasks/:wbsId/:level/:mother') .get(controller.getTasks) .put(controller.fixTasks); - taskRouter.route('/task/:id').post(controller.postTask).get(controller.getTaskById); + wbsRouter.route('/task/:id') + .post(controller.postTask) + .get(controller.getTaskById); - taskRouter.route('/task/import/:id').post(controller.importTask); + wbsRouter.route('/task/import/:id') + .post(controller.importTask); - taskRouter.route('/task/del/:taskId/:mother').post(controller.deleteTask); + wbsRouter.route('/task/del/:taskId/:mother') + .post(controller.deleteTask); - taskRouter.route('/task/wbs/:wbsId').get(controller.getWBSId); + wbsRouter.route('/task/wbs/:wbsId') + .get(controller.getWBSId); - taskRouter.route('/task/wbs/del/:wbsId').post(controller.deleteTaskByWBS); + wbsRouter.route('/task/wbs/del/:wbsId') + .post(controller.deleteTaskByWBS); - taskRouter.route('/task/update/:taskId').put(controller.updateTask); + wbsRouter.route('/task/update/:taskId') + .put(controller.updateTask); - taskRouter.route('/task/updateStatus/:taskId').put(controller.updateTaskStatus); + wbsRouter.route('/task/updateStatus/:taskId') + .put(controller.updateTaskStatus); - taskRouter.route('/task/updateAllParents/:wbsId/').put(controller.updateAllParents); + wbsRouter.route('/task/updateAllParents/:wbsId/') + .put(controller.updateAllParents); - taskRouter.route('/tasks/swap/').put(controller.swap); + wbsRouter.route('/tasks/swap/') + .put(controller.swap); - taskRouter.route('/tasks/update/num').put(controller.updateNum); + wbsRouter.route('/tasks/update/num') + .put(controller.updateNum); - taskRouter.route('/tasks/moveTasks/:wbsId').put(controller.moveTask); + wbsRouter.route('/tasks/moveTasks/:wbsId') + .put(controller.moveTask); - taskRouter.route('/tasks/user/:userId').get(controller.getTasksByUserId); + wbsRouter.route('/tasks/user/:userId') + .get(controller.getTasksByUserId); - taskRouter.route('/user/:userId/teams/tasks').get(controller.getTasksForTeamsByUser); + wbsRouter.route('/user/:userId/teams/tasks') + .get(controller.getTasksForTeamsByUser); - taskRouter.route('/tasks/reviewreq/:userId').post(controller.sendReviewReq); + wbsRouter.route('/tasks/reviewreq/:userId') + .post(controller.sendReviewReq); - return taskRouter; + return wbsRouter; }; module.exports = routes; diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index 1bf8cfc44..dd940504c 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -5,25 +5,19 @@ const router = function (team) { const teamRouter = express.Router(); - teamRouter - .route('/team') + teamRouter.route('/team') .get(controller.getAllTeams) - .post(controller.postTeam) - .put(controller.updateTeamVisibility); + .post(controller.postTeam); - teamRouter - .route('/team/:teamId') + teamRouter.route('/team/:teamId') .get(controller.getTeamById) .put(controller.putTeam) .delete(controller.deleteTeam); - teamRouter - .route('/team/:teamId/users/') + teamRouter.route('/team/:teamId/users/') .post(controller.assignTeamToUsers) .get(controller.getTeamMembership); - teamRouter.route('/teamCode').get(controller.getAllTeamCode); - return teamRouter; }; diff --git a/src/routes/timeentryRouter.js b/src/routes/timeentryRouter.js index 10ec0d6d0..88f203e94 100644 --- a/src/routes/timeentryRouter.js +++ b/src/routes/timeentryRouter.js @@ -17,8 +17,6 @@ const routes = function (TimeEntry) { TimeEntryRouter.route('/TimeEntry/users').post(controller.getTimeEntriesForUsersList); - TimeEntryRouter.route('/TimeEntry/reports').post(controller.getTimeEntriesForReports); - TimeEntryRouter.route('/TimeEntry/lostUsers').post(controller.getLostTimeEntriesForUserList); TimeEntryRouter.route('/TimeEntry/lostProjects').post( diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index e9c458b1b..02e9eac9c 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -1,10 +1,9 @@ const { body } = require('express-validator'); const express = require('express'); -const { ValidationError } = require('../utilities/errorHandling/customError'); -const routes = function (userProfile, project) { - const controller = require('../controllers/userProfileController')(userProfile, project); +const routes = function (userProfile) { + const controller = require('../controllers/userProfileController')(userProfile); const userProfileRouter = express.Router(); @@ -12,31 +11,19 @@ const routes = function (userProfile, project) { .route('/userProfile') .get(controller.getUserProfiles) .post( - body('firstName').customSanitizer((value) => { - if (!value) throw new ValidationError('First Name is required'); - return value.trim(); - }), - body('lastName').customSanitizer((value) => { - if (!value) throw new ValidationError('Last Name is required'); - return value.trim(); - }), + body('firstName').customSanitizer(value => value.trim()), + body('lastName').customSanitizer(value => value.trim()), controller.postUserProfile, ); userProfileRouter .route('/userProfile/:userId') .get(controller.getUserById) - .put( - body('firstName').customSanitizer((value) => { - if (!value) throw new ValidationError('First Name is required'); - return value.trim(); - }), - body('lastName').customSanitizer((value) => { - if (!value) throw new ValidationError('Last Name is required'); - return value.trim(); - }), - body('personalLinks').customSanitizer((value) => - value.map((link) => { + .put( + body('firstName').customSanitizer((req) => req.trim()), + body('lastName').customSanitizer((req) => req.trim()), + body('personalLinks').customSanitizer((req) => + req.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, @@ -44,11 +31,11 @@ const routes = function (userProfile, project) { Link: link.Link.replace(/\s/g, ''), }; } - throw new ValidationError('personalLinks not valid'); + throw new Error('Url not valid'); }), ), - body('adminLinks').customSanitizer((value) => - value.map((link) => { + body('adminLinks').customSanitizer((req) => + req.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, @@ -56,7 +43,7 @@ const routes = function (userProfile, project) { Link: link.Link.replace(/\s/g, ''), }; } - throw new ValidationError('adminLinks not valid'); + throw new Error('Url not valid'); }), ), controller.putUserProfile, @@ -100,7 +87,8 @@ const routes = function (userProfile, project) { .route('/userProfile/authorizeUser/weeeklySummaries') .post(controller.authorizeUser); - userProfileRouter.route('/userProfile/projects/:name').get(controller.getProjectsByPerson); + + userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); return userProfileRouter; }; diff --git a/src/routes/wbsRouter.js b/src/routes/wbsRouter.js index 8a646c757..a5eb2d126 100644 --- a/src/routes/wbsRouter.js +++ b/src/routes/wbsRouter.js @@ -6,9 +6,15 @@ const routes = function (wbs) { wbsRouter.route('/wbs/:projectId').get(controller.getAllWBS); - wbsRouter.route('/wbs/:id').post(controller.postWBS).delete(controller.deleteWBS); + wbsRouter.route('/wbs/:id') + .post(controller.postWBS) + .delete(controller.deleteWBS); - wbsRouter.route('/wbsId/:id').get(controller.getWBSById); + wbsRouter.route('/wbsId/:id') + .get(controller.getWBSById); + + wbsRouter.route('/wbs/user/:userId') + .get(controller.getWBSByUserId); wbsRouter.route('/wbs').get(controller.getWBS); diff --git a/src/server.js b/src/server.js index e53949703..43c6fec6f 100644 --- a/src/server.js +++ b/src/server.js @@ -1,12 +1,14 @@ /* eslint-disable quotes */ require('dotenv').load(); -const { app, logger } = require('./app'); +const { app, logger, Sentry } = require('./app'); const websockets = require('./websockets').default; require('./startup/db')(); require('./cronjobs/userProfileJobs')(); +// The error handler must be before any other error middleware and after all controllers +app.use(Sentry.Handlers.errorHandler()); const port = process.env.PORT || 4500; const server = app.listen(port, () => { diff --git a/src/services/userService.js b/src/services/userService.js deleted file mode 100644 index 401a32671..000000000 --- a/src/services/userService.js +++ /dev/null @@ -1,37 +0,0 @@ -const mongoose = require('mongoose'); -const UserProfileModel = require('../models/userProfile'); -const logger = require('../startup/logger'); -/** - * This function take a list of user email and return a list of user profiles projection that only contains the user ID and user email. - * @param {Array} userEmails A list of user email - * @returns {Array} A list of user profiles projection that only contains the user ID and user email. - */ -async function getUserIdAndEmailByEmails(userEmails) { - if (!Array.isArray(userEmails)) { - throw new Error('Invalid user email list'); - } - try { - return await UserProfileModel.find({ email: { $in: userEmails } }, '_id email'); - } catch (error) { - throw new Error(`Could not fetch user profiles: ${error.message}`); - } -} - -/** - * This function takes a user ID and returns the name of the user. - * @param {*} userId - * @returns {mongoose.Model} The user profile projection contains the first/last name, and email of the user. - */ -async function getUserFullNameAndEmailById(userId) { - try { - return await UserProfileModel.findById(userId, 'firstName lastName email'); - } catch (error) { - logger.logException(error, 'Error getting user full name'); - return null; - } -} - -module.exports = { - getUserIdAndEmailByEmails, - getUserFullNameAndEmailById, -}; diff --git a/src/startup/logger.js b/src/startup/logger.js index 56d6c5bb1..4892a6caa 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -1,7 +1,6 @@ /* eslint-disable no-console */ const Sentry = require('@sentry/node'); const { extraErrorDataIntegration } = require('@sentry/integrations'); -const { v4: uuidv4 } = require('uuid'); // Read more about intergration plugins here: https://docs.sentry.io/platforms/node/configuration/integrations/pluggable-integrations/ exports.init = function () { @@ -62,33 +61,22 @@ exports.init = function () { Sentry.setTag('app_name', 'hgn-backend'); }; -exports.logInfo = function (message, extraDataObject = null) { +exports.logInfo = function (message) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment console.log(message); - return 'LocalEnvriomentHasNoTrackingId'; + } else { + Sentry.captureMessage(message, { level: 'info' }); } - return Sentry.captureMessage(message, (scope) => { - scope.setExtras({ extraDataObject }); - scope.setLevel('info'); - return scope; - }); }; /** - * Send log message to Sentry if in production or development environment. Otherwise, log to console. * * @param {Error} error error object to be logged to Sentry * @param {String} transactionName (Optional) name assigned to a transaction. Seachable in Sentry (e.g. error in Function/Service/Operation/Job name) * @param {*} extraData (Optional) extra data to be logged to Sentry (e.g. request body, params, message, etc.) - * @param {String} trackingId (Optional) unique id to track the error in Sentry. Search by tag 'tacking_id' */ -exports.logException = function ( - error, - transactionName = null, - extraData = null, - trackingId = null, -) { +exports.logException = function (error, transactionName = null, extraData = null) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment console.error(error); @@ -96,9 +84,6 @@ exports.logException = function ( `Additional info \ntransactionName : ${transactionName} \nextraData: ${JSON.stringify(extraData)}`, ); } else { - if (trackingId == null) { - trackingId = uuidv4(); - } Sentry.captureException(error, (scope) => { if (transactionName !== null) { scope.setTransactionName(transactionName); @@ -106,9 +91,7 @@ exports.logException = function ( if (extraData !== null) { scope.setExtra('extraData', extraData); } - scope.setTag('tracking_id', trackingId); return scope; }); } - return trackingId; }; diff --git a/src/startup/routes.js b/src/startup/routes.js index 82a4155a8..77078bb2c 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -18,7 +18,6 @@ const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); // Title const title = require('../models/title'); -const blueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); const weeklySummaryAIPrompt = require('../models/weeklySummaryAIPrompt'); const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); @@ -44,10 +43,11 @@ const { buildingTool, buildingEquipment, } = require('../models/bmdashboard/buildingInventoryItem'); +// const buildingTool = require('../models/bmdashboard/buildingTool'); const timeOffRequest = require('../models/timeOffRequest'); const followUp = require('../models/followUp'); -const userProfileRouter = require('../routes/userProfileRouter')(userProfile, project); +const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const warningRouter = require('../routes/warningRouter')(userProfile); const badgeRouter = require('../routes/badgeRouter')(badge); const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); @@ -116,14 +116,10 @@ const bmInventoryTypeRouter = require('../routes/bmdashboard/bmInventoryTypeRout ); const titleRouter = require('../routes/titleRouter')(title); -const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool, toolType); +const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool); const bmEquipmentRouter = require('../routes/bmdashboard/bmEquipmentRouter')(buildingEquipment); const bmIssueRouter = require('../routes/bmdashboard/bmIssueRouter')(buildingIssue); -const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssignmentRouter')( - blueSquareEmailAssignment, - userProfile, -); module.exports = function (app) { app.use('/api', forgotPwdRouter); @@ -161,7 +157,6 @@ module.exports = function (app) { app.use('/api', titleRouter); app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); - app.use('/api', blueSquareEmailAssignmentRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); diff --git a/src/test/assertions.js b/src/test/assertions.js index cc3bcb900..db12b8cae 100644 --- a/src/test/assertions.js +++ b/src/test/assertions.js @@ -1,5 +1,4 @@ const assertResMock = (statusCode, message, response, mockRes) => { - console.log(mockRes); expect(mockRes.status).toHaveBeenCalledWith(statusCode); expect(mockRes.send).toHaveBeenCalledWith(message); expect(response).toBeUndefined(); diff --git a/src/test/createTestPermissions.js b/src/test/createTestPermissions.js index 58623ea3f..691f2cd5d 100644 --- a/src/test/createTestPermissions.js +++ b/src/test/createTestPermissions.js @@ -36,18 +36,14 @@ const permissionsRoles = [ 'putTeam', 'assignTeamToUsers', // Time Entries - 'editTimeEntryTime', - 'editTimeEntryDate', - 'editTimeEntryDescription', - 'editTimeEntryToggleTangible', + 'editTimeEntry', 'deleteTimeEntry', - 'postTimeEntry', + // 'postTimeEntry',? // User Profile 'putRole', 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', - 'updateSummaryRequirements', 'changeUserStatus', 'updatePassword', 'deleteUserProfile', @@ -188,19 +184,14 @@ const permissionsRoles = [ 'deleteTeam', 'putTeam', 'assignTeamToUsers', - 'editTimeEntryTime', - 'editTimeEntryDescription', - 'editTimeEntryDate', - 'editTimeEntryToggleTangible', + 'editTimeEntry', 'deleteTimeEntry', - 'postTimeEntry', 'updatePassword', 'getUserProfiles', 'getProjectMembers', 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', - 'updateSummaryRequirements', 'deleteUserProfile', 'infringementAuthorizer', 'postWbs', diff --git a/src/test/db/createUser.js b/src/test/db/createUser.js index ce487ccd6..e7c06aebc 100644 --- a/src/test/db/createUser.js +++ b/src/test/db/createUser.js @@ -5,9 +5,9 @@ const createUser = async () => { up.password = 'SuperSecretPassword@'; up.role = 'Administrator'; - up.firstName = 'requestor_first_name'; - up.lastName = 'requestor_last_name'; - up.jobTitle = ['any_job_title']; + up.firstName = 'Requestor_first_name'; + up.lastName = 'Requestor_last_name'; + up.jobTitle = ['Any_job_title']; up.phoneNumber = ['123456789']; up.bio = 'any_bio'; up.weeklycommittedHours = 21; @@ -32,8 +32,8 @@ const createUser = async () => { up.location = { userProvided: '', coords: { - lat: null, - lng: null, + lat: 51, + lng: 110, }, country: '', city: '', @@ -46,11 +46,12 @@ const createUser = async () => { up.isFirstTimelog = true; up.actualEmail = ''; up.isVisible = true; + up.totalTangibleHrs = 10; - /* - remove hard coded _id field to allow MongoDB to + /* + remove hard coded _id field to allow MongoDB to automatically create a unique id for us. - Now this function is more reusable if we + Now this function is more reusable if we need to create more than 1 user. */ diff --git a/src/test/mock-response.js b/src/test/mock-response.js index 336e64057..057ee45d8 100644 --- a/src/test/mock-response.js +++ b/src/test/mock-response.js @@ -1,7 +1,6 @@ const mockRes = { status: jest.fn().mockReturnThis(), send: jest.fn(), - json: jest.fn(), }; module.exports = mockRes; diff --git a/src/utilities/addMembersToTeams.js b/src/utilities/addMembersToTeams.js index bca402f60..b637fa2c1 100644 --- a/src/utilities/addMembersToTeams.js +++ b/src/utilities/addMembersToTeams.js @@ -11,28 +11,21 @@ const UserProfile = require('../models/userProfile'); const Teams = require('../models/team'); const addMembersField = async () => { - await Teams.updateMany({}, { $set: { members: [] } }).catch((error) => - logger.logException('Error adding field:', error), - ); + await Teams.updateMany({}, { $set: { members: [] } }).catch(error => logger.logException('Error adding field:', error)); const allUsers = await UserProfile.find({}); const updateOperations = allUsers .map((user) => { const { _id, teams, createdDate } = user; - return teams.map((team) => - Teams.updateOne( - { _id: team }, - { $addToSet: { members: { userId: _id, addDateTime: createdDate, visibility: true } } }, - ), - ); + return teams.map(team => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate } } })); }) .flat(); - await Promise.all(updateOperations).catch((error) => logger.logException(error)); + await Promise.all(updateOperations).catch(error => logger.logException(error)); }; const deleteMembersField = async () => { - await Teams.updateMany({}, { $unset: { members: '' } }).catch((err) => console.error(err)); + await Teams.updateMany({}, { $unset: { members: '' } }).catch(err => console.error(err)); }; const run = () => { @@ -49,7 +42,7 @@ const run = () => { }) // .then(deleteMembersField) .then(addMembersField) - .catch((err) => logger.logException(err)) + .catch(err => logger.logException(err)) .finally(() => { mongoose.connection.close(); console.log('Done! ✅'); diff --git a/src/utilities/constants.js b/src/utilities/constants.js deleted file mode 100644 index 5afa9dee0..000000000 --- a/src/utilities/constants.js +++ /dev/null @@ -1,14 +0,0 @@ -// Constants used throughout the application. -const constants = { - ALLOWED_EMAIL_ACCOUNT: ['jae@onecommunityglobal.org', 'one.community@me.com', 'jsabol@me.com'], - PROTECTED_EMAIL_ACCOUNT: [ - 'jae@onecommunityglobal.org', - 'one.community@me.com', - 'jsabol@me.com', - 'devadmin@hgn.net', - ], - - // Add more constants here -}; - -module.exports = constants; diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 43dfec2a0..e0b6560f5 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -37,10 +37,7 @@ const permissionsRoles = [ 'putTeam', 'assignTeamToUsers', // Time Entries - 'editTimeEntryTime', - 'editTimeEntryDescription', - 'editTimeEntryDate', - 'editTimeEntryToggleTangible', + 'editTimeEntry', 'deleteTimeEntry', 'postTimeEntry', // User Profile @@ -53,10 +50,8 @@ const permissionsRoles = [ 'updatePassword', 'deleteUserProfile', 'infringementAuthorizer', - 'manageAdminLinks', 'manageTimeOffRequests', 'changeUserRehireableStatus', - 'updateSummaryRequirements', // WBS 'postWbs', 'deleteWbs', @@ -80,7 +75,7 @@ const permissionsRoles = [ 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', - + // Title 'seeQSC', 'addNewTitle', @@ -88,6 +83,7 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'editTeamCode', + ], }, { @@ -206,10 +202,7 @@ const permissionsRoles = [ 'deleteTeam', 'putTeam', 'assignTeamToUsers', - 'editTimeEntryTime', - 'editTimeEntryDescription', - 'editTimeEntryDate', - 'editTimeEntryToggleTangible', + 'editTimeEntry', 'deleteTimeEntry', 'postTimeEntry', 'updatePassword', @@ -218,7 +211,6 @@ const permissionsRoles = [ 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', - 'updateSummaryRequirements', 'deleteUserProfile', 'infringementAuthorizer', 'postWbs', @@ -251,7 +243,7 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'changeUserRehireableStatus', - 'manageAdminLinks', + ], }, ]; @@ -300,7 +292,7 @@ const createInitialPermissions = async () => { } // Update Default presets - const defaultName = 'hard-coded default'; + const defaultName = 'hard-coded default' const presetDataBase = allPresets.find( (preset) => preset.roleName === roleName && preset.presetName === defaultName, diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index eb8eca3de..655eacaea 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -56,23 +56,15 @@ const closure = () => { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(null, result); } - // Prevent logging email in production - // Why? - // 1. Could create a security risk - // 2. Could create heavy loads on the server if emails are sent to many people - // 3. Contain limited useful info: - // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} - if (process.env.NODE_ENV === 'local') { - logger.logInfo(`Email sent: ${JSON.stringify(result)}`); - } + logger.logInfo(result); } catch (error) { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(error, null); } logger.logException( error, - `Error sending email: from ${CLIENT_EMAIL} to ${recipient} subject ${subject}`, - `Extra Data: cc ${cc} bcc ${bcc}`, + `Error sending email: from ${CLIENT_EMAIL} to ${recipient}`, + `Extra Data: cc ${cc} bcc ${bcc} subject ${subject}`, ); } }, process.env.MAIL_QUEUE_INTERVAL || 1000); diff --git a/src/utilities/errorHandling/customError.js b/src/utilities/errorHandling/customError.js deleted file mode 100644 index 81e38f083..000000000 --- a/src/utilities/errorHandling/customError.js +++ /dev/null @@ -1,48 +0,0 @@ -/* eslint-disable max-classes-per-file */ -/** - * By throwing an instance of this class, the global error handler middleware will return the error message and status code. - */ -class CustomError extends Error { - statusCode; - - constructor(message, statusCode) { - super(message); - this.statusCode = statusCode; - this.name = this.constructor.name; - } -} - -class ValidationError extends CustomError { - constructor(message) { - super(message, 400); - } -} - -class AuthenticationError extends CustomError { - constructor(message) { - super(message, 401); - } -} - -class AuthorizationError extends CustomError { - constructor(message) { - super(message, 403); - } -} - -class RuntimeError extends CustomError { - constructor(message) { - super(message, 500); - } -} - -// Define other error classes here... - -module.exports = { - CustomError, - ValidationError, - AuthenticationError, - AuthorizationError, - RuntimeError, - // Export other error classes here... -}; diff --git a/src/utilities/errorHandling/globalErrorHandler.js b/src/utilities/errorHandling/globalErrorHandler.js deleted file mode 100644 index 90d3774f8..000000000 --- a/src/utilities/errorHandling/globalErrorHandler.js +++ /dev/null @@ -1,55 +0,0 @@ -/* eslint-disable no-console */ -const { v4: uuidv4 } = require('uuid'); -const { CustomError } = require('./customError'); -const Logger = require('../../startup/logger'); - -/** - * Custom error handler middleware for global unhandled errors. Make it the last middleware since it returns a response and do not call next(). - */ -function globalErrorHandler(err, req, res, next) { - /** - * Notes: - * 1. We will need to implement a global distributed eventId for tracking errors - * if move to microservices artechtecture or with replicated services - * 2. Developer will use the eventId (Searchable) to trace the error in the Sentry.io - */ - const trackingId = uuidv4(); - const errorMessage = `An internal error has occurred. If the issue persists, please contact the administrator and provide the trakcing ID: ${trackingId}`; - - let transactionName = ''; - const requestData = req.body && req.method ? JSON.stringify(req.body) : null; - - if (req.method) { - transactionName = transactionName.concat(req.method); - } - if (req.url) { - transactionName = transactionName.concat(' ', req.originalUrl); - } - - // transactionName = transactionName.concat(' ', 'Tracking ID: ', eventId); - if (!err) { - transactionName = - 'Critical: err parameter is missing. This is probably due to an improper error handling in the code.'; - } - // Log the error to Sentry if not in local environment - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { - Logger.logException(err, transactionName, requestData, trackingId); - } else { - console.log( - `An error occurred. Transaction: ${transactionName} \nRequest Data: ${requestData}`, - ); - console.error(err); - } - - // If the error is an instance of CustomError, return the error message and status code - if (err instanceof CustomError) { - return res.status(err.statusCode).json({ error: err.message, errorMessage }); - } - - // else return generic error message with tracking id and status code 500 - return res.status(500).json({ - errorMessage, - }); -} - -export default globalErrorHandler; diff --git a/src/utilities/exceptionHandler.js b/src/utilities/exceptionHandler.js new file mode 100644 index 000000000..9669f362a --- /dev/null +++ b/src/utilities/exceptionHandler.js @@ -0,0 +1,17 @@ +const logger = require('../startup/logger'); + +const exceptionHandler = (err, req, res, next) => { + logger.logException(err); + + const errStatus = err.statusCode || 500; + const errMsg = err.message || 'Internal Server Error. Please try again later. If the problem persists, please contact support ID.'; + res.status(errStatus).json({ + success: false, + status: errStatus, + message: errMsg, + stack: !process.env.NODE_ENV || process.env.NODE_ENV === 'local' ? err.stack : {}, + }); + next(); +}; + +export default exceptionHandler; diff --git a/src/utilities/htmlContentSanitizer.js b/src/utilities/htmlContentSanitizer.js index 51da82414..ccfb7cd61 100644 --- a/src/utilities/htmlContentSanitizer.js +++ b/src/utilities/htmlContentSanitizer.js @@ -1,8 +1,8 @@ const sanitizeHtml = require('sanitize-html'); // Please refer to https://www.npmjs.com/package/sanitize-html?activeTab=readme for more information. -// eslint-disable-next-line import/prefer-default-export +// eslint-disable-next-line import/prefer-default-export const cleanHtml = (dirty) => sanitizeHtml(dirty, { allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), @@ -10,4 +10,4 @@ const cleanHtml = (dirty) => module.exports = { cleanHtml, -}; \ No newline at end of file +}; diff --git a/src/utilities/nodeCache.js b/src/utilities/nodeCache.js index ac9a93543..7856a4c55 100644 --- a/src/utilities/nodeCache.js +++ b/src/utilities/nodeCache.js @@ -41,21 +41,11 @@ const cache = function () { return cacheStore.has(key); } - /** - * Reset or redefine the ttl of a key. If ttl is not passed or set to 0 it's similar to .del() - * @param {*} key - * @param {*} ttl - */ - function setKeyTimeToLive(key, ttl) { - cacheStore.ttl(key, ttl); - } - return { setCache, getCache, removeCache, hasCache, - setKeyTimeToLive, }; }; diff --git a/src/utilities/objectUtils.js b/src/utilities/objectUtils.js deleted file mode 100644 index 1c580cfe3..000000000 --- a/src/utilities/objectUtils.js +++ /dev/null @@ -1,60 +0,0 @@ -const _ = require('lodash'); - -function deepCopyMongooseObjectWithLodash(originalDoc) { - const plainObject = originalDoc.toObject({ getters: true, virtuals: false }); - const deepCopy = _.cloneDeep(plainObject); - return deepCopy; -} - - - -function filterFieldsFromObj(obj, keysToFilter) { - const filteredObj = {}; - // keys to exclude: sensitive data and verbose data - const keysToExclude = [ - '_id', - '__v', - 'password', - 'location', - 'privacySettings', - 'infringements', - 'badgeCollection', - 'copiedAiPrompt', - 'hoursByCategory', - 'savedTangibleHrs', - ]; - // a list of keys to filter from the object - Object.keys(obj).forEach((key) => { - if (keysToExclude.includes(key)) { - return; - } - if (keysToFilter.includes(key)) { - filteredObj[key] = obj[key]; - } - }); - - return filteredObj; -} - -/** - * Return two objects that have different values for the same key. - * @param {Object} originalDoc Must be a object - * @param {Object} updatedDoc - * @param {Array} keysToFilter - * @returns - */ -function returnObjectDifference(originalDoc, updatedDoc, keysToFilter) { - const originalDocFiltered = filterFieldsFromObj(originalDoc, keysToFilter); - const updatedDocFiltered = filterFieldsFromObj(updatedDoc, keysToFilter); - // filter out the keys that have the same value in both objects - const updatedObj = _.omitBy(updatedDocFiltered, (value, key) => - _.isEqual(value, originalDocFiltered[key]), - ); - const originalObj = _.omitBy(originalDocFiltered, (value, key) => - _.isEqual(value, updatedDocFiltered[key]), - ); - // return an object contains the difference between the original and updated document - return { originalObj, updatedObj }; -} - -module.exports = { deepCopyMongooseObjectWithLodash, filterFieldsFromObj, returnObjectDifference }; diff --git a/src/utilities/permission.spec.js b/src/utilities/permission.spec.js deleted file mode 100644 index 579d14988..000000000 --- a/src/utilities/permission.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -const { PROTECTED_EMAIL_ACCOUNT } = require('./constants'); -const { canRequestorUpdateUser } = require('./permissions'); -const userService = require('../services/userService'); - -// Mock modules -jest.mock('../startup/logger', () => ({ - logException: jest.fn(), - logInfo: jest.fn(), // Add any other mocked methods if needed -})); -jest.mock('../services/userService'); -jest.mock('./nodeCache', () => - jest.fn().mockImplementation(() => ({ - hasCache: jest.fn(), - getCache: jest.fn(), - setCache: jest.fn(), - })), -); - -// Mock function return -const mockGetUserIdAndEmailByEmails = (value) => - jest - .spyOn(userService, 'getUserIdAndEmailByEmails') - .mockImplementationOnce(() => Promise.resolve(value)); - -describe('canRequestorUpdateUser', () => { - let serverCache; - - beforeEach(() => { - jest.clearAllMocks(); - serverCache = require('./nodeCache')(); - }); - - it('should return true if requestorId is not in protectedEmailAccountIds and targetUserId is also not in protectedEmailAccountIds', async () => { - serverCache.hasCache.mockReturnValue(false); - mockGetUserIdAndEmailByEmails([ - { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, - { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, - { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, - { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, - ]); - - const result = await canRequestorUpdateUser('nonProctedId_1', 'nonProctedId_2'); - expect(result).toBe(true); - }); - - it('should return true if requestorId is in protectedEmailAccountIds and targetUserId is also in protectedEmailAccountIds', async () => { - serverCache.hasCache.mockReturnValue(false); - mockGetUserIdAndEmailByEmails([ - { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, - { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, - { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, - { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, - ]); - - const result = await canRequestorUpdateUser('protectedUserId_1', 'protectedUserId_2'); - expect(result).toBe(true); - }); - - it('should return false if requestorId is not in protectedEmailAccountIds and targetUserId is in protectedEmailAccountIds', async () => { - serverCache.hasCache.mockReturnValue(false); - mockGetUserIdAndEmailByEmails([ - { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, - { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, - { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, - { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, - ]); - - const result = await canRequestorUpdateUser('nonProctedId_1', 'protectedUserId_2'); - expect(result).toBe(false); - }); - - it('should return true if requestorId is in protectedEmailAccountIds and targetUserId is not in protectedEmailAccountIds', async () => { - serverCache.hasCache.mockReturnValue(false); - mockGetUserIdAndEmailByEmails([ - { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, - { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, - { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, - { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, - ]); - - const result = await canRequestorUpdateUser('protectedUserId_2', 'nonProctedId_1'); - expect(result).toBe(true); - }); -}); diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 2299e8812..ff522900e 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -1,90 +1,36 @@ const Role = require('../models/role'); const UserProfile = require('../models/userProfile'); -const serverCache = require('./nodeCache')(); -const userService = require('../services/userService'); -const Logger = require('../startup/logger'); -const { PROTECTED_EMAIL_ACCOUNT, ALLOWED_EMAIL_ACCOUNT } = require('./constants'); -const hasRolePermission = async (role, action) => - Role.findOne({ roleName: role }) - .exec() - .then(({ permissions }) => permissions.includes(action)) - .catch(false); +const hasRolePermission = async (role, action) => Role.findOne({ roleName: role }) + .exec() + .then(({ permissions }) => permissions.includes(action)) + .catch(false); -const hasIndividualPermission = async (userId, action) => - UserProfile.findById(userId) - .select('permissions') - .exec() - .then(({ permissions }) => permissions.frontPermissions.includes(action)) - .catch(false); +const hasIndividualPermission = async (userId, action) => UserProfile.findById(userId) + .select('permissions') + .exec() + .then(({ permissions }) => permissions.frontPermissions.includes(action)) + .catch(false); -const hasPermission = async (requestor, action) => - (await hasRolePermission(requestor.role, action)) || - hasIndividualPermission(requestor.requestorId, action); +const hasPermission = async (requestor, action) => await hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); -function getDistinct(arr1, arr2) { - // Merge arrays and reduce to distinct elements - const distinctArray = arr1.concat(arr2).reduce((acc, curr) => { - if (acc.indexOf(curr) === -1) { - acc.push(curr); - } - return acc; - }, []); - - return distinctArray; -} -/** - * Check if requestor can update specific Jae related user. Return false if requestor not allowed to update. Otherwise, return true. - * @param {*} requestorId - * @param {*} userId - * @returns - */ -const canRequestorUpdateUser = async (requestorId, targetUserId) => { - let protectedEmailAccountIds; - let allowedEmailAccountIds; - const emailToQuery = getDistinct(PROTECTED_EMAIL_ACCOUNT, ALLOWED_EMAIL_ACCOUNT); - // Persist the list of protected email accounts in the application cache - if ( - !serverCache.hasCache('protectedEmailAccountIds') || - !serverCache.hasCache('allowedEmailAccountIds') - ) { - try { - // get the user info by email accounts - const query = await userService.getUserIdAndEmailByEmails(emailToQuery); - // Check if all protected email accounts were found - if (query.length !== emailToQuery.length) { - // find out which email accounts were not found - const notFoundEmails = emailToQuery.filter( - (entity) => !query.map(({ email }) => email).includes(entity), - ); - Logger.logInfo( - `The following protected email accounts were not found in the ${process.env.NODE_ENV} database: ${notFoundEmails.join(', ')}.`, - ); - } - // Find out a list of protected email account ids and allowed email id - allowedEmailAccountIds = query - .filter(({ email }) => ALLOWED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); - protectedEmailAccountIds = query - .filter(({ email }) => PROTECTED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); - - serverCache.setCache('protectedEmailAccountIds', protectedEmailAccountIds); - serverCache.setCache('allowedEmailAccountIds', allowedEmailAccountIds); - // Redefine time to live to 1 hour for this specific key - serverCache.setKeyTimeToLive('protectedEmailAccountIds', 60 * 60); - serverCache.setKeyTimeToLive('allowedEmailAccountIds', 60 * 60); - } catch (error) { - Logger.logException(error, 'Error getting protected email accounts'); - } - } else { - protectedEmailAccountIds = serverCache.getCache('protectedEmailAccountIds'); - allowedEmailAccountIds = serverCache.getCache('allowedEmailAccountIds'); - } - // Check requestor edit permission and check target user is protected or not. - return !( - protectedEmailAccountIds.includes(targetUserId) && !allowedEmailAccountIds.includes(requestorId) - ); +const canRequestorUpdateUser = (requestorId, userId) => { + const allowedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org + '5baac381e16814009017678c', // dev one.community@me.com + '63fe855b7186de1898fa8ab7', // dev jsabol@me.com + '64deba9064131f13540ac23b', // main jae@onecommunityglobal.org + '610d5ae67002ae3fecdf7080', // main one.community@me.com + '63fe8e4fa79c5619d0b5a563', // main jsabol@me.com + ]; + const protectedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org + '5baac381e16814009017678c', // dev one.community@me.com + '63fe855b7186de1898fa8ab7', // dev jsabol@me.com + '64deba9064131f13540ac23b', // main jae@onecommunityglobal.org + '610d5ae67002ae3fecdf7080', // main one.community@me.com + '63fe8e4fa79c5619d0b5a563', // main jsabol@me.com + '64c17eb8c737b05dd4ac4e28', // dev devadmin@hgn.net + ]; + return !(protectedIds.includes(userId) && !allowedIds.includes(requestorId)); }; module.exports = { hasPermission, canRequestorUpdateUser }; diff --git a/src/utilities/timeUtils.js b/src/utilities/timeUtils.js index 285f5dce2..9239a38de 100644 --- a/src/utilities/timeUtils.js +++ b/src/utilities/timeUtils.js @@ -26,4 +26,4 @@ module.exports = { formatCreatedDate, DAY_OF_WEEK, getDayOfWeekStringFromUTC, -}; \ No newline at end of file +}; diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 32c4168f5..4fed5334c 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -34,10 +34,9 @@ const action = { PAUSE_TIMER: 'PAUSE_TIMER', STOP_TIMER: 'STOP_TIMER', CLEAR_TIMER: 'CLEAR_TIMER', - GET_TIMER: 'GET_TIMER', - SET_GOAL: 'SET_GOAL', - ADD_GOAL: 'ADD_TO_GOAL', - REMOVE_GOAL: 'REMOVE_FROM_GOAL', + SET_GOAL: 'SET_GOAL=', + ADD_GOAL: 'ADD_TO_GOAL=', + REMOVE_GOAL: 'REMOVE_FROM_GOAL=', FORCED_PAUSE: 'FORCED_PAUSE', ACK_FORCED: 'ACK_FORCED', START_CHIME: 'START_CHIME', @@ -67,8 +66,6 @@ const startTimer = (client) => { }; const pauseTimer = (client, forced = false) => { - if (client.paused) return; - client.time = updatedTimeSinceStart(client); if (client.time === 0) client.chiming = true; client.startAt = moment.invalid(); // invalid can not be saved in database @@ -77,8 +74,8 @@ const pauseTimer = (client, forced = false) => { }; const startChime = (client, msg) => { - const state = msg.value; - client.chiming = state === true; + const state = msg.split('=')[1]; + client.chiming = state === 'true'; }; const ackForcedPause = (client) => { @@ -110,27 +107,17 @@ const clearTimer = (client) => { }; const setGoal = (client, msg) => { - const newGoal = parseInt(msg.value); + const newGoal = parseInt(msg.split('=')[1]); client.goal = newGoal; client.time = newGoal; client.initialGoal = newGoal; }; const addGoal = (client, msg) => { - const duration = parseInt(msg.value); + const duration = parseInt(msg.split('=')[1]); const goalAfterAddition = moment.duration(client.goal).add(duration, 'milliseconds').asHours(); - if (goalAfterAddition >= MAX_HOURS) { - const oldGoal = client.goal; - client.goal = MAX_HOURS * 60 * 60 * 1000; - client.time = moment - .duration(client.time) - .add(client.goal - oldGoal, 'milliseconds') - .asMilliseconds() - .toFixed(); - - return; - } + if (goalAfterAddition > MAX_HOURS) return; client.goal = moment .duration(client.goal) @@ -145,7 +132,7 @@ const addGoal = (client, msg) => { }; const removeGoal = (client, msg) => { - const duration = parseInt(msg.value); + const duration = parseInt(msg.split('=')[1]); const goalAfterRemoval = moment .duration(client.goal) .subtract(duration, 'milliseconds') @@ -170,30 +157,27 @@ const removeGoal = (client, msg) => { }; const handleMessage = async (msg, clients, userId) => { - // if (!clients.has(userId)) { - // throw new Error('It should have this user in memory'); - // } + if (!clients.has(userId)) { + throw new Error('It should have this user in memory'); + } - const client = await getClient(clients, userId); + const client = clients.get(userId); let resp = null; - switch (msg.action) { + switch (msg) { case action.START_TIMER: startTimer(client); break; - case action.GET_TIMER: - resp = client; - break; - case action.SET_GOAL: + case msg.match(/SET_GOAL=/i)?.input: setGoal(client, msg); break; - case action.ADD_GOAL: + case msg.match(/ADD_TO_GOAL=/i)?.input: addGoal(client, msg); break; - case action.REMOVE_GOAL: + case msg.match(/REMOVE_FROM_GOAL=/i)?.input: removeGoal(client, msg); break; - case action.START_CHIME: + case msg.match(/START_CHIME=/i)?.input: startChime(client, msg); break; case action.PAUSE_TIMER: @@ -214,7 +198,7 @@ const handleMessage = async (msg, clients, userId) => { default: resp = { ...client, - error: `Unknown operation ${msg.action}, please use one from { ${Object.values(action).join(', ')} }`, + error: `Unknown operation ${msg}, please use one from { ${Object.values(action).join(', ')} }`, }; break; } diff --git a/src/websockets/index.js b/src/websockets/index.js index 368f07ba2..a12fa18cb 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -3,31 +3,35 @@ /* eslint-disable consistent-return */ /* eslint-disable quotes */ /* eslint-disable linebreak-style */ -const WebSocket = require('ws'); -const moment = require('moment'); -const jwt = require('jsonwebtoken'); -const config = require('../config'); +const WebSocket = require("ws"); +const moment = require("moment"); +const jwt = require("jsonwebtoken"); +const config = require("../config"); const { - insertNewUser, - removeConnection, - broadcastToSameUser, - hasOtherConn, -} = require('./TimerService/connectionsHandler'); -const { getClient, handleMessage, action } = require('./TimerService/clientsHandler'); + insertNewUser, + removeConnection, + broadcastToSameUser, + hasOtherConn, +} = require("./TimerService/connectionsHandler"); +const { + getClient, + handleMessage, + action, +} = require("./TimerService/clientsHandler"); /** - * Here we authenticate the user. - * We get the token from the headers and try to verify it. - * If it fails, we throw an error. - * Else we check if the token is valid and if it is, we return the user id. - */ +* Here we authenticate the user. +* We get the token from the headers and try to verify it. +* If it fails, we throw an error. +* Else we check if the token is valid and if it is, we return the user id. +*/ const authenticate = (req, res) => { - const authToken = req.headers?.['sec-websocket-protocol']; - let payload = ''; + const authToken = req.headers?.["sec-websocket-protocol"]; + let payload = ""; try { payload = jwt.verify(authToken, config.JWT_SECRET); } catch (error) { - res('401 Unauthorized', null); + res("401 Unauthorized", null); } if ( @@ -37,34 +41,34 @@ const authenticate = (req, res) => { !payload.role || moment().isAfter(payload.expiryTimestamp) ) { - res('401 Unauthorized', null); + res("401 Unauthorized", null); } res(null, payload.userid); }; /** - * Here we start the timer service. - * First we create a map to store the clients and start the Websockets Server. - * Then we set the upgrade event listener to the Express Server, authenticate the user and - * if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. - */ +* Here we start the timer service. +* First we create a map to store the clients and start the Websockets Server. +* Then we set the upgrade event listener to the Express Server, authenticate the user and +* if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. +*/ export default async (expServer) => { const wss = new WebSocket.Server({ noServer: true, - path: '/timer-service', + path: "/timer-service", }); - expServer.on('upgrade', (request, socket, head) => { + expServer.on("upgrade", (request, socket, head) => { authenticate(request, (err, client) => { if (err || !client) { - socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); socket.destroy(); return; } request.userId = client; wss.handleUpgrade(request, socket, head, (websocket) => { - wss.emit('connection', websocket, request); + wss.emit("connection", websocket, request); }); }); }); @@ -72,11 +76,11 @@ export default async (expServer) => { const clients = new Map(); // { userId: timerInfo } const connections = new Map(); // { userId: connections[] } - wss.on('connection', async (ws, req) => { + wss.on("connection", async (ws, req) => { ws.isAlive = true; const { userId } = req; - ws.on('pong', () => { + ws.on("pong", () => { ws.isAlive = true; }); @@ -89,33 +93,32 @@ export default async (expServer) => { ws.send(JSON.stringify(clientTimer)); /** - * Here we handle the messages from the client. - * And we broadcast the response to all the clients that are connected to the same user. - */ - ws.on('message', async (data) => { - const msg = JSON.parse(data.toString()); - if (msg.action === action.HEARTBEAT) { - ws.send(JSON.stringify({ heartbeat: 'pong' })); + * Here we handle the messages from the client. + * And we broadcast the response to all the clients that are connected to the same user. + */ + ws.on("message", async (data) => { + const msg = data.toString(); + if (msg === action.HEARTBEAT) { + ws.send(JSON.stringify({ heartbeat: "pong" })); return; } - const resp = await handleMessage(msg, clients, msg.userId ?? userId); + const resp = await handleMessage(msg, clients, userId); broadcastToSameUser(connections, userId, resp); - if (msg.userId) broadcastToSameUser(connections, msg.userId, resp); }); /** - * Here we handle the close event. - * If there is another connection to the same user, we don't do anything. - * Else he is the last connection and we do a forced pause if need be. - * This may happen if the user closes all the tabs or the browser or he lost connection with - * the service - * We then remove the connection from the connections map. - */ - ws.on('close', async () => { + * Here we handle the close event. + * If there is another connection to the same user, we don't do anything. + * Else he is the last connection and we do a forced pause if need be. + * This may happen if the user closes all the tabs or the browser or he lost connection with + * the service + * We then remove the connection from the connections map. + */ + ws.on("close", async () => { if (!hasOtherConn(connections, userId, ws)) { const client = clients.get(userId); if (client.started && !client.paused) { - await handleMessage({ action: action.FORCED_PAUSE }, clients, userId); + await handleMessage(action.FORCED_PAUSE, clients, userId); } } removeConnection(connections, userId, ws); From f966f8af6f93b3b512f6ab8f8165784e17c2905e Mon Sep 17 00:00:00 2001 From: One Community Date: Wed, 14 Aug 2024 21:12:44 -0700 Subject: [PATCH 02/11] Revert "Revert "Revert "Revert "Revert "Backend Release to Main [1.92]""""" --- README.md | 2 + package-lock.json | 6264 ++++++++--------- package.json | 6 +- .../dashBoardController/dashboarddata.md | 12 + .../editSuggestionOption.md | 17 + .../dashBoardController/getAIPrompt.md | 17 + .../getPromptCopiedDate.md | 14 + .../getSuggestionOption.md | 15 + .../dashBoardController/leaderboarddata.md | 15 + .../dashBoardController/monthlydata.md | 14 + requirements/dashBoardController/orgData.md | 14 + .../dashBoardController/sendBugReport.md | 14 + .../dashBoardController/sendMakeSuggestion.md | 14 + .../dashBoardController/updateAIPrompt.md | 16 + .../dashBoardController/updateCopiedPrompt.md | 16 + .../dashBoardController/weeklydata.md | 13 + requirements/forcePwdController/forcePwd.md | 14 + .../forgotPwdController/postForgotPwd.md | 16 + .../inventoryController/getAllInvInProject.md | 17 - .../getAllInvInProjectWBS.md | 17 - .../postInvInProjectWBS.md | 19 - .../logincontroller/getUser-usecase.md | 9 + requirements/logincontroller/login-usecase.md | 21 + .../mapLocationsController/deleteLocation.md | 17 - .../mapLocationsController/getAllLocations.md | 17 - .../mapLocationsController/putUserLocation.md | 17 - .../updateUserLocation.md | 19 - .../createMouseoverText.md | 11 + .../getMouseoverText.md | 11 + .../updateMouseoverText.md | 12 + .../creatUserNotification.md | 15 + .../deleteUserNotification.md | 14 + .../getSentNotifications.md | 14 + .../getUnreadUserNotifications.md | 15 + .../getUserNotifications.md | 15 + .../markNotificationAsRead.md | 14 + .../deleteOwnerMessage.md | 13 + .../ownerMessageController/getOwnerMessage.md | 14 + .../updateOwnerMessage.md | 13 + .../rolePresetsController/createNewPresets.md | 18 + .../rolePresetsController/deletePresetById.md | 17 + .../rolePresetsController/getPresetsByRole.md | 15 + .../rolePresetsController/updatePresetById.md | 17 + requirements/rolesController/createNewRole.md | 24 + .../rolesController/deleteRoleById.md | 15 + requirements/rolesController/getAllRoles.md | 11 + requirements/rolesController/getRolesById.md | 14 + .../rolesController/updateRoleById.md | 28 + .../deleteTimeOffRequestById.md | 21 + .../getTimeOffRequestById.md | 15 + .../getTimeOffRequests.md | 16 + .../setTimeOffRequest.md | 20 + .../updateTimeOffRequestById.md | 26 + src/app.js | 11 +- .../BlueSquareEmailAssignmentController.js | 67 + src/controllers/badgeController.js | 91 +- src/controllers/badgeController.spec.js | 518 +- .../bmdashboard/bmEquipmentController.js | 40 + .../bmdashboard/bmInventoryTypeController.js | 116 +- .../bmdashboard/bmReusableController.js | 153 +- .../bmdashboard/bmToolController.js | 141 +- src/controllers/dashBoardController.js | 4 +- src/controllers/dashBoardController.spec.js | 838 +++ src/controllers/forcePwdController.spec.js | 68 + src/controllers/forgotPwdcontroller.spec.js | 183 + src/controllers/inventoryController.js | 6 +- src/controllers/inventoryController.spec.js | 334 - src/controllers/logincontroller.js | 66 +- src/controllers/logincontroller.spec.js | 211 + src/controllers/mapLocationsController.js | 41 +- .../mapLocationsController.spec.js | 367 - src/controllers/mouseoverTextController.js | 78 +- .../mouseoverTextController.spec.js | 162 + src/controllers/notificationController.js | 60 +- .../notificationController.spec.js | 313 + .../ownerMessageController.spec.js | 139 + .../profileInitialSetupController.js | 318 +- src/controllers/projectController.js | 282 +- src/controllers/reportsController.js | 285 +- src/controllers/rolePresetsController.js | 43 +- src/controllers/rolePresetsController.spec.js | 444 ++ src/controllers/rolesController.js | 98 +- src/controllers/rolesController.spec.js | 229 + src/controllers/taskController.js | 128 +- .../taskEditSuggestionController.js | 32 +- src/controllers/teamController.js | 82 + src/controllers/timeEntryController.js | 567 +- .../timeOffRequestController.spec.js | 1260 ++++ src/controllers/titleController.js | 14 +- src/controllers/userProfileController.js | 458 +- src/controllers/wbsController.js | 55 +- src/cronjobs/userProfileJobs.js | 1 - src/helpers/dashboardhelper.js | 276 +- src/helpers/helperModels/userProjects.js | 17 - src/helpers/overviewReportHelper.js | 645 ++ src/helpers/overviewReportHelper.spec.js | 64 + src/helpers/taskHelper.js | 333 +- src/helpers/userHelper.js | 575 +- src/models/BlueSquareEmailAssignment.js | 10 + .../bmdashboard/buildingInventoryItem.js | 13 +- .../bmdashboard/buildingInventoryType.js | 22 +- src/models/project.js | 18 +- src/models/team.js | 12 +- src/models/timeentry.js | 1 + src/models/userProfile.js | 32 +- src/models/wbs.js | 2 - src/routes/BlueSquareEmailAssignmentRouter.js | 18 + src/routes/badgeRouter.js | 19 +- src/routes/bmdashboard/bmEquipmentRouter.js | 2 + .../bmdashboard/bmInventoryTypeRouter.js | 4 + src/routes/bmdashboard/bmReusableRouter.js | 6 + src/routes/bmdashboard/bmToolRouter.js | 10 +- src/routes/forgotPwdRouter.test.js | 83 + src/routes/mapLocationsRouter.test.js | 200 - src/routes/mouseoverTextRouter.test.js | 98 + src/routes/reportsRouter.js | 30 +- src/routes/rolePresetRouter.test.js | 238 + src/routes/taskRouter.js | 50 +- src/routes/teamRouter.js | 14 +- src/routes/timeentryRouter.js | 2 + src/routes/userProfileRouter.js | 42 +- src/routes/wbsRouter.js | 10 +- src/server.js | 4 +- src/services/userService.js | 37 + src/startup/logger.js | 25 +- src/startup/routes.js | 11 +- src/test/assertions.js | 1 + src/test/createTestPermissions.js | 15 +- src/test/db/createUser.js | 17 +- src/test/mock-response.js | 1 + src/utilities/addMembersToTeams.js | 17 +- src/utilities/constants.js | 14 + src/utilities/createInitialPermissions.js | 20 +- src/utilities/emailSender.js | 14 +- src/utilities/errorHandling/customError.js | 48 + .../errorHandling/globalErrorHandler.js | 55 + src/utilities/exceptionHandler.js | 17 - src/utilities/htmlContentSanitizer.js | 4 +- src/utilities/nodeCache.js | 10 + src/utilities/objectUtils.js | 60 + src/utilities/permission.spec.js | 84 + src/utilities/permissions.js | 108 +- src/utilities/timeUtils.js | 2 +- src/websockets/TimerService/clientsHandler.js | 54 +- src/websockets/index.js | 99 +- 145 files changed, 12882 insertions(+), 6048 deletions(-) create mode 100644 requirements/dashBoardController/dashboarddata.md create mode 100644 requirements/dashBoardController/editSuggestionOption.md create mode 100644 requirements/dashBoardController/getAIPrompt.md create mode 100644 requirements/dashBoardController/getPromptCopiedDate.md create mode 100644 requirements/dashBoardController/getSuggestionOption.md create mode 100644 requirements/dashBoardController/leaderboarddata.md create mode 100644 requirements/dashBoardController/monthlydata.md create mode 100644 requirements/dashBoardController/orgData.md create mode 100644 requirements/dashBoardController/sendBugReport.md create mode 100644 requirements/dashBoardController/sendMakeSuggestion.md create mode 100644 requirements/dashBoardController/updateAIPrompt.md create mode 100644 requirements/dashBoardController/updateCopiedPrompt.md create mode 100644 requirements/dashBoardController/weeklydata.md create mode 100644 requirements/forcePwdController/forcePwd.md create mode 100644 requirements/forgotPwdController/postForgotPwd.md delete mode 100644 requirements/inventoryController/getAllInvInProject.md delete mode 100644 requirements/inventoryController/getAllInvInProjectWBS.md delete mode 100644 requirements/inventoryController/postInvInProjectWBS.md create mode 100644 requirements/logincontroller/getUser-usecase.md create mode 100644 requirements/logincontroller/login-usecase.md delete mode 100644 requirements/mapLocationsController/deleteLocation.md delete mode 100644 requirements/mapLocationsController/getAllLocations.md delete mode 100644 requirements/mapLocationsController/putUserLocation.md delete mode 100644 requirements/mapLocationsController/updateUserLocation.md create mode 100644 requirements/mouseoverTextController/createMouseoverText.md create mode 100644 requirements/mouseoverTextController/getMouseoverText.md create mode 100644 requirements/mouseoverTextController/updateMouseoverText.md create mode 100644 requirements/notificationController/creatUserNotification.md create mode 100644 requirements/notificationController/deleteUserNotification.md create mode 100644 requirements/notificationController/getSentNotifications.md create mode 100644 requirements/notificationController/getUnreadUserNotifications.md create mode 100644 requirements/notificationController/getUserNotifications.md create mode 100644 requirements/notificationController/markNotificationAsRead.md create mode 100644 requirements/ownerMessageController/deleteOwnerMessage.md create mode 100644 requirements/ownerMessageController/getOwnerMessage.md create mode 100644 requirements/ownerMessageController/updateOwnerMessage.md create mode 100644 requirements/rolePresetsController/createNewPresets.md create mode 100644 requirements/rolePresetsController/deletePresetById.md create mode 100644 requirements/rolePresetsController/getPresetsByRole.md create mode 100644 requirements/rolePresetsController/updatePresetById.md create mode 100644 requirements/rolesController/createNewRole.md create mode 100644 requirements/rolesController/deleteRoleById.md create mode 100644 requirements/rolesController/getAllRoles.md create mode 100644 requirements/rolesController/getRolesById.md create mode 100644 requirements/rolesController/updateRoleById.md create mode 100644 requirements/timeOffRequestController/deleteTimeOffRequestById.md create mode 100644 requirements/timeOffRequestController/getTimeOffRequestById.md create mode 100644 requirements/timeOffRequestController/getTimeOffRequests.md create mode 100644 requirements/timeOffRequestController/setTimeOffRequest.md create mode 100644 requirements/timeOffRequestController/updateTimeOffRequestById.md create mode 100644 src/controllers/BlueSquareEmailAssignmentController.js create mode 100644 src/controllers/dashBoardController.spec.js create mode 100644 src/controllers/forcePwdController.spec.js create mode 100644 src/controllers/forgotPwdcontroller.spec.js delete mode 100644 src/controllers/inventoryController.spec.js create mode 100644 src/controllers/logincontroller.spec.js delete mode 100644 src/controllers/mapLocationsController.spec.js create mode 100644 src/controllers/mouseoverTextController.spec.js create mode 100644 src/controllers/notificationController.spec.js create mode 100644 src/controllers/ownerMessageController.spec.js create mode 100644 src/controllers/rolePresetsController.spec.js create mode 100644 src/controllers/rolesController.spec.js create mode 100644 src/controllers/timeOffRequestController.spec.js delete mode 100644 src/helpers/helperModels/userProjects.js create mode 100644 src/helpers/overviewReportHelper.js create mode 100644 src/helpers/overviewReportHelper.spec.js create mode 100644 src/models/BlueSquareEmailAssignment.js create mode 100644 src/routes/BlueSquareEmailAssignmentRouter.js create mode 100644 src/routes/forgotPwdRouter.test.js delete mode 100644 src/routes/mapLocationsRouter.test.js create mode 100644 src/routes/mouseoverTextRouter.test.js create mode 100644 src/routes/rolePresetRouter.test.js create mode 100644 src/services/userService.js create mode 100644 src/utilities/constants.js create mode 100644 src/utilities/errorHandling/customError.js create mode 100644 src/utilities/errorHandling/globalErrorHandler.js delete mode 100644 src/utilities/exceptionHandler.js create mode 100644 src/utilities/objectUtils.js create mode 100644 src/utilities/permission.spec.js diff --git a/README.md b/README.md index 90890f72a..bbea44b69 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ SMTPPort= SMTPUser= TOKEN_LIFETIME= TOKEN_LIFETIME_UNITS= +NODE_ENV= `local` | `development` | `production`
    JWT_SECRET= + To make the process easy create a .env file and put the above text in the file and replace values with the correct values, which you can get from your teammates. Then do an npm run-script build followed by an npm start. By default, the services will start on port 4500 and you can http://localhost:4500/api/ to access the methods. A tools like Postman will be your best friend here, you will need to have an auth token placed in the 'Authorization' header which you can get through the networking tab of the local frontend when you login. - `npm run lint` -- fix lint diff --git a/package-lock.json b/package-lock.json index fd52a1746..3c4e3e99e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,9 +71,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -89,9 +89,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -135,9 +135,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -180,9 +180,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -609,6 +609,23 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true + } + } + }, "@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", @@ -673,6 +690,23 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true + } + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", @@ -911,9 +945,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -1057,9 +1091,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -1248,16 +1282,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1425,19 +1449,42 @@ "dev": true }, "@jest/console": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", - "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^26.6.2", - "jest-util": "^26.6.2", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1496,41 +1543,64 @@ } }, "@jest/core": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", - "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/reporters": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", + "ci-info": "^3.2.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^26.6.2", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-resolve-dependencies": "^26.6.3", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "jest-watcher": "^26.6.2", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "rimraf": "^3.0.0", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1571,6 +1641,31 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -1589,75 +1684,40 @@ } }, "@jest/environment": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", - "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", - "dev": true, - "requires": { - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2" - } - }, - "@jest/fake-timers": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", - "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@sinonjs/fake-timers": "^6.0.1", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" - } - }, - "@jest/globals": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", - "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", - "dev": true, - "requires": { - "@jest/environment": "^26.6.2", - "@jest/types": "^26.6.2", - "expect": "^26.6.2" - } - }, - "@jest/reporters": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", - "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^7.0.0" + "jest-mock": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1698,18 +1758,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "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 - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1721,82 +1769,70 @@ } } }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, "requires": { - "@sinclair/typebox": "^0.27.8" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" } }, - "@jest/source-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", - "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" + "jest-get-type": "^29.6.3" }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true } } }, - "@jest/test-result": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", - "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", - "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", - "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", - "dev": true, - "requires": { - "@jest/test-result": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3" - } - }, - "@jest/transform": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^26.6.2", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-util": "^26.6.2", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1837,18 +1873,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "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 - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1860,19 +1884,41 @@ } } }, - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1924,717 +1970,1130 @@ } } }, - "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - } + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@nicolo-ribaudo/chokidar-2": { - "version": "2.1.8-no-fsevents.3", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", - "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", - "optional": true - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@sinclair/typebox": "^0.27.8" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@redis/bloom": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", - "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==" - }, - "@redis/client": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", - "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", - "requires": { - "cluster-key-slot": "1.1.0", - "generic-pool": "3.8.2", - "yallist": "4.0.0" + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" }, "dependencies": { - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } } } }, - "@redis/graph": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", - "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==" - }, - "@redis/json": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", - "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==" - }, - "@redis/search": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", - "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==" - }, - "@redis/time-series": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", - "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==" - }, - "@sentry-internal/tracing": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.0.tgz", - "integrity": "sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==", - "requires": { - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" - } - }, - "@sentry/core": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.0.tgz", - "integrity": "sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==", - "requires": { - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" - } - }, - "@sentry/integrations": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.110.0.tgz", - "integrity": "sha512-cWpEGMTyX1XO4jb0NXMh1thkkiSajM5ydE/ceAdxmG9V7gv7E1pREK8P1NeVvzvjZ67z+uVWYbgYwXxd4eqZ/A==", - "requires": { - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0", - "localforage": "^1.8.1" - } - }, - "@sentry/node": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.110.0.tgz", - "integrity": "sha512-YPfweCSzo/omnx5q1xOEZfI8Em3jnPqj7OM4ObXmoSKEK+kM1oUF3BTRzw5BJOaOCSTBFY1RAsGyfVIyrwxWnA==", - "requires": { - "@sentry-internal/tracing": "7.110.0", - "@sentry/core": "7.110.0", - "@sentry/types": "7.110.0", - "@sentry/utils": "7.110.0" - } - }, - "@sentry/types": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.0.tgz", - "integrity": "sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==" - }, - "@sentry/utils": { - "version": "7.110.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.0.tgz", - "integrity": "sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==", - "requires": { - "@sentry/types": "7.110.0" - } - }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@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" - } - }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "dev": true - }, - "@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, "requires": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, "dependencies": { - "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", - "dev": true + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "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 }, - "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "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, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "has-flag": "^4.0.0" } } } }, - "@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "dependencies": { + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } } }, - "@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, "requires": { - "@babel/types": "^7.20.7" + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" }, "dependencies": { - "@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true }, - "@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", "dev": true }, - "@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "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 + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "requires": { - "@types/connect": "*", - "@types/node": "*" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "@types/bson": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", - "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "requires": { - "@types/node": "*" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "dependencies": { + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + } + } + } } }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, + "@jridgewell/resolve-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", + "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", + "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", "requires": { - "@types/node": "*" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "@types/cookiejar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", - "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", - "dev": true + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "optional": true }, - "@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==", + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "@types/express-serve-static-core": { - "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" } }, - "@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } + "@redis/bloom": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", + "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==" }, - "@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, + "@redis/client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", + "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", "requires": { - "@types/istanbul-lib-report": "*" + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "yallist": "4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } } }, - "@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", - "dev": true, - "requires": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" - } + "@redis/graph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", + "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==" }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true + "@redis/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", + "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==" }, - "@types/methods": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", - "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", - "dev": true + "@redis/search": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", + "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==" }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true + "@redis/time-series": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", + "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==" }, - "@types/mongodb": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", - "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "@sentry-internal/tracing": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.110.0.tgz", + "integrity": "sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==", "requires": { - "@types/bson": "*", - "@types/node": "*" + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" } }, - "@types/node": { - "version": "8.10.66", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", - "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" - }, - "@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true - }, - "@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true - }, - "@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==", - "dev": true - }, - "@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==", - "dev": true - }, - "@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==", - "dev": true, + "@sentry/core": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.110.0.tgz", + "integrity": "sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==", "requires": { - "@types/mime": "^1", - "@types/node": "*" + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" } }, - "@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "@types/superagent": { - "version": "8.1.6", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", - "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", - "dev": true, + "@sentry/integrations": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.110.0.tgz", + "integrity": "sha512-cWpEGMTyX1XO4jb0NXMh1thkkiSajM5ydE/ceAdxmG9V7gv7E1pREK8P1NeVvzvjZ67z+uVWYbgYwXxd4eqZ/A==", "requires": { - "@types/cookiejar": "^2.1.5", - "@types/methods": "^1.1.4", - "@types/node": "*" + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0", + "localforage": "^1.8.1" } }, - "@types/supertest": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", - "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", - "dev": true, + "@sentry/node": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.110.0.tgz", + "integrity": "sha512-YPfweCSzo/omnx5q1xOEZfI8Em3jnPqj7OM4ObXmoSKEK+kM1oUF3BTRzw5BJOaOCSTBFY1RAsGyfVIyrwxWnA==", "requires": { - "@types/methods": "^1.1.4", - "@types/superagent": "^8.1.0" + "@sentry-internal/tracing": "7.110.0", + "@sentry/core": "7.110.0", + "@sentry/types": "7.110.0", + "@sentry/utils": "7.110.0" } }, - "@types/tmp": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", - "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", - "dev": true + "@sentry/types": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.110.0.tgz", + "integrity": "sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==" }, - "@types/yargs": { - "version": "15.0.19", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", - "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", - "dev": true, + "@sentry/utils": { + "version": "7.110.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.110.0.tgz", + "integrity": "sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==", "requires": { - "@types/yargs-parser": "*" + "@sentry/types": "7.110.0" } }, - "@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true - }, - "abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "requires": { - "event-target-shim": "^5.0.0" + "type-detect": "4.0.8" } }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@sinonjs/commons": "^3.0.0" } }, - "acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" }, "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "dev": true - } - } - }, - "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 - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "dev": true + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, "requires": { - "debug": "4" + "@babel/types": "^7.0.0" } }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "@types/babel__traverse": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", + "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "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" + "@babel/types": "^7.20.7" + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + } } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", "dev": true, "requires": { - "type-fest": "^0.21.3" + "@types/connect": "*", + "@types/node": "*" } }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "@types/bson": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", + "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "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==", + "@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==", + "dev": true, "requires": { - "color-convert": "^1.9.0" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" } }, - "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" } }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, - "aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "requires": { - "dequal": "^2.0.3" + "@types/istanbul-lib-coverage": "*" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "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==", + "@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", "dev": true }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, - "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", - "dev": true, + "@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "@types/bson": "*", + "@types/node": "*" } }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" }, - "array-includes": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", - "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "@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==", + "dev": true + }, + "@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==", + "dev": true + }, + "@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==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", - "is-string": "^1.0.7" + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "@types/superagent": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", + "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", + "dev": true, + "requires": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz", + "integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==", + "dev": true, + "requires": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, + "@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.19", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.19.tgz", + "integrity": "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "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 + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "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 + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "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, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "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 + }, + "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==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "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 + }, + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" }, "dependencies": { "define-properties": { @@ -2792,12 +3251,6 @@ } } }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true - }, "array.prototype.findlastindex": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", @@ -3824,12 +4277,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -3855,12 +4302,6 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "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 - }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -4232,9 +4673,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -4290,42 +4731,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "base": { - "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", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - } - } - } - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4402,19 +4807,23 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" + }, + "dependencies": { + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "requires": { + "to-regex-range": "^5.0.1" + } + } } }, - "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 - }, "browserslist": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.0.tgz", @@ -4472,23 +4881,6 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "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, - "requires": { - "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" - } - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4511,18 +4903,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001576", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001576.tgz", - "integrity": "sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==" - }, - "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, - "requires": { - "rsvp": "^4.8.4" - } + "version": "1.0.30001646", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz", + "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==" }, "chalk": { "version": "2.4.2", @@ -4556,40 +4939,17 @@ } }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true }, "cjs-module-lexer": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", - "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, - "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, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -4663,51 +5023,14 @@ } }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, "requires": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" } }, "clone": { @@ -4742,16 +5065,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4848,12 +5161,6 @@ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true - }, "core-js": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", @@ -4889,6 +5196,95 @@ "vary": "^1" } }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "cron": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/cron/-/cron-1.8.2.tgz", @@ -4925,74 +5321,12 @@ } } }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dev": true, - "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } - } - }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", - "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" - }, - "dependencies": { - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - } - } - }, "data-view-buffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", @@ -5136,22 +5470,10 @@ "ms": "2.1.2" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true }, "deep-is": { @@ -5183,28 +5505,6 @@ "object-keys": "^1.0.12" } }, - "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, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - } - } - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5276,23 +5576,6 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, "domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -5341,9 +5624,9 @@ "integrity": "sha512-Gs7xVpIZ7tYYSDA+WgpzwpPvfGwUk3KSIjJ0akuj5XQHFdyQnsUoM76EA4CIHXNLPiVwTwOFay9RMb0ChG3OBw==" }, "emittery": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", - "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true }, "emoji-regex": { @@ -5507,33 +5790,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" }, - "escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "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 - } - } - }, "eslint": { "version": "8.47.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", @@ -6469,12 +6725,6 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", - "dev": true - }, "execa": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", @@ -6562,131 +6812,62 @@ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, "requires": { - "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" - }, - "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" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } - } - }, - "express": { - "version": "4.17.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", - "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.19.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.4.2", - "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.7", - "qs": "6.9.7", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.17.2", - "serve-static": "1.14.2", - "setprototypeof": "1.2.0", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "dependencies": { + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + } + } + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "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.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "dependencies": { "debug": { @@ -6700,7 +6881,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "safe-buffer": { "version": "5.2.1", @@ -6730,73 +6911,6 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "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, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "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", - "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" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - } - } - } - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6861,14 +6975,6 @@ "flat-cache": "^3.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==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -6894,7 +7000,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } }, @@ -6957,23 +7063,6 @@ "is-callable": "^1.1.3" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, "formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -7045,15 +7134,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7076,9 +7156,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "optional": true }, "function-bind": { @@ -7178,12 +7258,6 @@ "get-intrinsic": "^1.1.1" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true - }, "glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -7339,13 +7413,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", - "dev": true, - "optional": true - }, "gtoken": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", @@ -7422,58 +7489,6 @@ "has-symbols": "^1.0.2" } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7502,21 +7517,6 @@ "parse-passwd": "^1.0.0" } }, - "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 - }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7546,17 +7546,6 @@ "toidentifier": "1.0.1" } }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - } - }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -7720,15 +7709,6 @@ "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==" }, - "is-accessor-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", - "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, "is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -7785,26 +7765,11 @@ "has-tostringtag": "^1.0.0" } }, - "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 - }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, - "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, - "requires": { - "ci-info": "^2.0.0" - } - }, "is-core-module": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", @@ -7813,15 +7778,6 @@ "has": "^1.0.3" } }, - "is-data-descriptor": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", - "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, "is-data-view": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", @@ -7913,29 +7869,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-descriptor": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", - "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7993,12 +7926,6 @@ "isobject": "^3.0.1" } }, - "is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8043,12 +7970,6 @@ "which-typed-array": "^1.1.11" } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -8057,22 +7978,6 @@ "call-bind": "^1.0.2" } }, - "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 - }, - "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, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8096,27 +8001,360 @@ "dev": true }, "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==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dev": true, "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "dependencies": { - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "3.0.1", + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true + }, + "@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "dev": true + }, + "@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + } + } + }, + "browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.815", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.815.tgz", + "integrity": "sha512-OvpTT2ItpOXJL7IGcYakRjHCt8L5GrrN/wHCQsRB4PQa1X9fe+X9oen245mIId7s14xvArCGSTIq644yPUKKLg==", + "dev": true + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, @@ -8132,24 +8370,357 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "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 + } + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "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, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + } + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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, "requires": { - "semver": "^7.5.3" + "yocto-queue": "^0.1.0" } }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8161,46 +8732,59 @@ } } }, - "istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "dependencies": { - "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 - } - } - }, - "istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", - "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "requires": { - "@jest/core": "^26.6.3", - "import-local": "^3.0.2", - "jest-cli": "^26.6.3" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8241,27 +8825,43 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-cli": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "requires": { - "@jest/core": "^26.6.3", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^26.6.3", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "prompts": "^2.0.1", - "yargs": "^15.4.1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8273,137 +8873,121 @@ } } }, - "jest-changed-files": { + "jest-diff": { "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", - "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "execa": "^4.0.0", - "throat": "^5.0.0" + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" }, "dependencies": { - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "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, "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" + "color-convert": "^2.0.1" } }, - "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==", + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "pump": "^3.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "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, "requires": { - "path-key": "^3.0.0" + "color-name": "~1.1.4" } }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "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 + }, + "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==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, - "jest-config": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", - "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^26.6.3", - "@jest/types": "^26.6.2", - "babel-jest": "^26.6.3", - "chalk": "^4.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^26.6.2", - "jest-environment-node": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-jasmine2": "^26.6.3", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2" + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "babel-jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, "requires": { - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.6.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" } }, - "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" + "@types/yargs-parser": "*" } }, - "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "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, "requires": { - "babel-plugin-jest-hoist": "^26.6.2", - "babel-preset-current-node-syntax": "^1.0.0" + "color-convert": "^2.0.1" } }, "chalk": { @@ -8437,10 +9021,35 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "supports-color": { @@ -8454,18 +9063,43 @@ } } }, - "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==", + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8517,28 +9151,55 @@ } } }, - "jest-docblock": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", - "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", - "dev": true, - "requires": { - "detect-newline": "^3.0.0" - } + "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 }, - "jest-each": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8590,87 +9251,57 @@ } } }, - "jest-environment-jsdom": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", - "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", - "dev": true, - "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2", - "jsdom": "^16.4.0" - } - }, - "jest-environment-node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", - "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + } } }, - "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 - }, - "jest-haste-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", - "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^26.6.2", - "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "throat": "^5.0.0" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "dependencies": { "ansi-styles": { @@ -8707,12 +9338,61 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8724,28 +9404,46 @@ } } }, - "jest-leak-detector": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", - "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", - "dev": true, - "requires": { - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" - } - }, - "jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8786,6 +9484,37 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8797,23 +9526,40 @@ } } }, - "jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -8854,12 +9600,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8871,16 +9611,6 @@ } } }, - "jest-mock": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", - "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*" - } - }, "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -8888,24 +9618,25 @@ "dev": true }, "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true }, "jest-resolve": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", - "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^26.6.2", - "read-pkg-up": "^7.0.1", - "resolve": "^1.18.1", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", "slash": "^3.0.0" }, "dependencies": { @@ -8967,44 +9698,67 @@ } }, "jest-resolve-dependencies": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", - "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-snapshot": "^26.6.2" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" } }, "jest-runner": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", - "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.7.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-docblock": "^26.0.0", - "jest-haste-map": "^26.6.2", - "jest-leak-detector": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9045,6 +9799,31 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "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, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "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 + }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9057,40 +9836,58 @@ } }, "jest-runtime": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", - "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", - "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/globals": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/yargs": "^15.0.0", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", "chalk": "^4.0.0", - "cjs-module-lexer": "^0.6.0", + "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.4.1" + "strip-bom": "^4.0.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9154,40 +9951,57 @@ } } }, - "jest-serializer": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", - "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.4" - } - }, "jest-snapshot": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", - "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.0.0", + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-haste-map": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "natural-compare": "^1.4.0", - "pretty-format": "^26.6.2", - "semver": "^7.3.2" + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9222,21 +10036,67 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "requires": { - "lru-cache": "^6.0.0" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9249,19 +10109,42 @@ } }, "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9314,19 +10197,42 @@ } }, "jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^26.6.2" + "pretty-format": "^29.7.0" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9373,6 +10279,37 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -9385,20 +10322,44 @@ } }, "jest-watcher": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", - "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "requires": { - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^26.6.2", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "dependencies": { + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9451,14 +10412,15 @@ } }, "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "requires": { "@types/node": "*", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" + "supports-color": "^8.0.0" }, "dependencies": { "has-flag": { @@ -9468,9 +10430,9 @@ "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" @@ -9492,75 +10454,6 @@ "argparse": "^2.0.1" } }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", - "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", - "dev": true, - "requires": { - "punycode": "^2.1.1" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", - "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - } - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "dev": true - } - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9617,9 +10510,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -10134,21 +11027,6 @@ "tmpl": "1.0.5" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, "md5-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", @@ -10230,27 +11108,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, - "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" - }, - "dependencies": { - "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, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -10525,25 +11382,6 @@ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" }, - "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", - "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" - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10576,12 +11414,6 @@ } } }, - "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-cache": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", @@ -10623,40 +11455,6 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, - "node-notifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - }, - "dependencies": { - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "optional": true - } - } - }, "node-releases": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", @@ -10729,18 +11527,6 @@ "abbrev": "1" } }, - "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, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -10763,48 +11549,11 @@ } } }, - "nwsapi": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.8.tgz", - "integrity": "sha512-GU/I3lTEFQ9mkEm07Q7HvdRajss8E1wVMGOk3/lHl60QPseG+B3BIQY+JUjYWw7gF8cCeoQCXd4N7DB7avw0Rg==", - "dev": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -10815,15 +11564,6 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", @@ -11354,15 +12094,6 @@ } } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "object.values": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", @@ -11576,18 +12307,6 @@ "type-check": "^0.4.0" } }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -11649,23 +12368,11 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true - }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true - }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -11740,12 +12447,6 @@ "find-up": "^3.0.0" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true - }, "possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -11855,146 +12556,57 @@ "ipaddr.js": "1.9.1" } }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, "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 }, - "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, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true + }, "qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==" }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "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==" - }, - "raw-body": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", - "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", - "requires": { - "bytes": "3.1.2", - "http-errors": "1.8.1", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - }, - "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 - }, - "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 - } + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "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==" + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" } }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -12056,16 +12668,6 @@ "@babel/runtime": "^7.8.4" } }, - "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, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, "regexp-clone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", @@ -12127,24 +12729,6 @@ } } }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true - }, "require-at": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", @@ -12156,18 +12740,6 @@ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true }, - "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==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "reselect": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz", @@ -12206,16 +12778,10 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true }, "reusify": { @@ -12239,12 +12805,6 @@ "glob": "^7.1.3" } }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12291,15 +12851,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, "safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -12330,214 +12881,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@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" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "braces": { - "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", - "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" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "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, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "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" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "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, - "requires": { - "pump": "^3.0.0" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true - }, - "micromatch": { - "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", - "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" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "which": { - "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" - } - } - } - }, "sanitize-html": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", @@ -12572,19 +12915,10 @@ "sparse-bitfield": "^3.0.3" } }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "send": { "version": "0.17.2", @@ -12639,12 +12973,6 @@ "send": "0.17.2" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12708,29 +13036,6 @@ } } }, - "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, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -12744,28 +13049,6 @@ "kind-of": "^6.0.2" } }, - "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, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "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 - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -12811,136 +13094,33 @@ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "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 - } - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" - }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" - }, - "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", - "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" - }, - "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" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "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, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-descriptor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", - "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.1", - "is-data-descriptor": "^1.0.1" - } - } - } - }, - "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, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" + "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 } } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==" + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -12951,19 +13131,6 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==" }, - "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", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -12980,12 +13147,6 @@ } } }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -12995,47 +13156,6 @@ "memory-pager": "^1.0.2" } }, - "spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true - }, - "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, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dev": true - }, - "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, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13059,27 +13179,6 @@ } } }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -13804,12 +13903,6 @@ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true - }, "strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -13942,44 +14035,11 @@ "has-flag": "^3.0.0" } }, - "supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "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==", - "dev": true, - "requires": { - "has-flag": "^4.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==" }, - "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 - }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -14017,16 +14077,6 @@ } } }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14044,16 +14094,10 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "dev": true - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, "tmp": { @@ -14073,38 +14117,6 @@ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "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, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -14127,18 +14139,6 @@ "nopt": "~1.0.10" } }, - "tough-cookie": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", - "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", - "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - } - }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14264,15 +14264,6 @@ "is-typed-array": "^1.1.9" } }, - "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, - "requires": { - "is-typedarray": "^1.0.0" - } - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -14314,65 +14305,31 @@ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==" }, - "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", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", "dev": true, "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true } } @@ -14386,33 +14343,11 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "dev": true - }, - "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, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" }, - "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 - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -14429,20 +14364,42 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "v8-to-istanbul": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz", - "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "requires": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "convert-source-map": "^2.0.0" }, "dependencies": { - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true } } @@ -14455,16 +14412,6 @@ "homedir-polyfill": "^1.0.1" } }, - "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, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "validator": { "version": "10.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", @@ -14475,24 +14422,6 @@ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "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, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", - "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" - } - }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -14507,21 +14436,6 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "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, - "requires": { - "iconv-lite": "0.4.24" - } - }, - "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 - }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -14552,12 +14466,6 @@ "is-symbol": "^1.0.3" } }, - "which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true - }, "which-typed-array": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", @@ -14652,38 +14560,24 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "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==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, "requires": { "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "signal-exit": "^3.0.7" } }, "ws": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==" - }, - "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 - }, - "xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==" }, "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -14698,69 +14592,25 @@ "dev": true }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^8.0.1", + "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": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "dependencies": { - "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, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "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, - "requires": { - "p-locate": "^4.1.0" - } - }, - "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, - "requires": { - "p-limit": "^2.2.0" - } - }, - "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 - } + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true }, "yauzl": { "version": "2.10.0", diff --git a/package.json b/package.json index 86c0e945d..8b88744dc 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "eslint-import-resolver-babel-module": "^5.3.1", "eslint-plugin-import": "^2.28.0", "husky": "^8.0.1", - "jest": "^26.6.0", + "jest": "^29.7.0", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.1", "eslint-plugin-react-hooks": "^4.6.0", @@ -69,7 +69,7 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.35", "mongodb": "^3.7.3", - "mongoose": "^5.13.15", + "mongoose": "^5.13.20", "mongoose-validator": "^2.1.0", "node-cache": "^5.1.2", "node-datetime": "^2.0.3", @@ -78,7 +78,7 @@ "sanitize-html": "^2.13.0", "supertest": "^6.3.4", "uuid": "^3.4.0", - "ws": "^8.8.1" + "ws": "^8.17.1" }, "nodemonConfig": { "watch": [ diff --git a/requirements/dashBoardController/dashboarddata.md b/requirements/dashBoardController/dashboarddata.md new file mode 100644 index 000000000..d2ec6a3b5 --- /dev/null +++ b/requirements/dashBoardController/dashboarddata.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if there is no error and return results + +> ## Negative case + +> ## Edge case diff --git a/requirements/dashBoardController/editSuggestionOption.md b/requirements/dashBoardController/editSuggestionOption.md new file mode 100644 index 000000000..5a0c1acbc --- /dev/null +++ b/requirements/dashBoardController/editSuggestionOption.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if suggestionData.field is added a new field +2. ✅ Returns 200 if suggestionData.suggestion is added a new suggestion +3. ✅ Returns 200 if suggestionData.field is deleted +4. ✅ Returns 200 if suggestionData.suggestion is deleted + +> ## Negative case + +1. ❌ Returns 500 if there is an error in the function + +> ## Edge case diff --git a/requirements/dashBoardController/getAIPrompt.md b/requirements/dashBoardController/getAIPrompt.md new file mode 100644 index 000000000..2db7d13bf --- /dev/null +++ b/requirements/dashBoardController/getAIPrompt.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if the GPT exists and send the results back +2. ✅ Returns 200 if there is no error and new GPT Prompt is created + +> ## Negative case + +1. ❌ Returns 500 if GPT Prompt does not exist +2. ❌ Returns 500 if there is an error in creating the GPT Prompt + +> ## Edge case \ No newline at end of file diff --git a/requirements/dashBoardController/getPromptCopiedDate.md b/requirements/dashBoardController/getPromptCopiedDate.md new file mode 100644 index 000000000..6b8bef08f --- /dev/null +++ b/requirements/dashBoardController/getPromptCopiedDate.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if there is a user and return copied AI prompt + +> ## Negative case + + +> ## Edge case +1. Returns undefined when the user is not found \ No newline at end of file diff --git a/requirements/dashBoardController/getSuggestionOption.md b/requirements/dashBoardController/getSuggestionOption.md new file mode 100644 index 000000000..cf2739853 --- /dev/null +++ b/requirements/dashBoardController/getSuggestionOption.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if there is suggestion data + +> ## Negative case + +1. ❌ Returns 404 if the suggestion data is not found +2. ❌ Returns 500 if there is an error in the function + +> ## Edge case diff --git a/requirements/dashBoardController/leaderboarddata.md b/requirements/dashBoardController/leaderboarddata.md new file mode 100644 index 000000000..6a7b36f0a --- /dev/null +++ b/requirements/dashBoardController/leaderboarddata.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if there is leaderboard data +2. ✅ Returns 200 if leaderboard data is empty and returns getUserLaborData + +> ## Negative case + +1. ❌ Returns 400 if there is an error + +> ## Edge case diff --git a/requirements/dashBoardController/monthlydata.md b/requirements/dashBoardController/monthlydata.md new file mode 100644 index 000000000..7d464c242 --- /dev/null +++ b/requirements/dashBoardController/monthlydata.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if there is no results and return empty results +3. ✅ Returns 200 if there is results and return results + +> ## Negative case + +> ## Edge case \ No newline at end of file diff --git a/requirements/dashBoardController/orgData.md b/requirements/dashBoardController/orgData.md new file mode 100644 index 000000000..2d147a950 --- /dev/null +++ b/requirements/dashBoardController/orgData.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if the result is found and returns result + +> ## Negative case + +1. ❌ Returns 400 if there is an error in the function + +> ## Edge case diff --git a/requirements/dashBoardController/sendBugReport.md b/requirements/dashBoardController/sendBugReport.md new file mode 100644 index 000000000..fd2f32c18 --- /dev/null +++ b/requirements/dashBoardController/sendBugReport.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if the bug report email is sent + +> ## Negative case + +1. ❌ Returns 500 if the email fails to send + +> ## Edge case diff --git a/requirements/dashBoardController/sendMakeSuggestion.md b/requirements/dashBoardController/sendMakeSuggestion.md new file mode 100644 index 000000000..1c425ac91 --- /dev/null +++ b/requirements/dashBoardController/sendMakeSuggestion.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ✅ Returns 200 if the suggestion email is sent successfully + +> ## Negative case + +1. ❌ Returns 500 if the suggestion email fails to send + +> ## Edge case diff --git a/requirements/dashBoardController/updateAIPrompt.md b/requirements/dashBoardController/updateAIPrompt.md new file mode 100644 index 000000000..1bb0202ec --- /dev/null +++ b/requirements/dashBoardController/updateAIPrompt.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if there is no error and AI Prompt is saved + +> ## Negative case + +1. ❌ Returns error 500 if the error occurs in the AI Prompt function + +> ## Edge case +1. Returns undefined if the requestor role is not an owner \ No newline at end of file diff --git a/requirements/dashBoardController/updateCopiedPrompt.md b/requirements/dashBoardController/updateCopiedPrompt.md new file mode 100644 index 000000000..bc84a91c8 --- /dev/null +++ b/requirements/dashBoardController/updateCopiedPrompt.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if there is no error and user is found + +> ## Negative case + +1. ❌ Returns error 404 if the user is not found +2. ❌ Returns error 500 if the error occurs in the file update function + +> ## Edge case diff --git a/requirements/dashBoardController/weeklydata.md b/requirements/dashBoardController/weeklydata.md new file mode 100644 index 000000000..e774ee2dc --- /dev/null +++ b/requirements/dashBoardController/weeklydata.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if there is no error and labordata is found + +> ## Negative case + +> ## Edge case diff --git a/requirements/forcePwdController/forcePwd.md b/requirements/forcePwdController/forcePwd.md new file mode 100644 index 000000000..ae1c5ae02 --- /dev/null +++ b/requirements/forcePwdController/forcePwd.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# ForcePwd + +> ## Negative Cases + +1. ✅ Returns a `400 Bad Request` status if userId is not valid with an error message "Bad Request". +2. ✅ Returns a `500 Internal Error` status with the error details if finding userProfile fails. +3. ✅ Returns a `500 Internal Error` status with the error details if new password fails to save. + +> ## Positive Cases + +1. ✅ Returns a `200 OK` status with a success message "password Reset". diff --git a/requirements/forgotPwdController/postForgotPwd.md b/requirements/forgotPwdController/postForgotPwd.md new file mode 100644 index 000000000..0d5349a26 --- /dev/null +++ b/requirements/forgotPwdController/postForgotPwd.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Forgot Pwd + +> ## Positive case + +1. ✅ Receives a POST request in the **/api/forgotpassword** route. +2. ✅ Returns **200** if successfully temporary password generated. + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist. +2. ✅ Returns 400 user does not exists in database. +3. ✅ Returns 500 if error encountered fetching user details from database. +4. ✅ Returns 500 if error encountered while saving temporary password. \ No newline at end of file diff --git a/requirements/inventoryController/getAllInvInProject.md b/requirements/inventoryController/getAllInvInProject.md deleted file mode 100644 index fdb38b0d2..000000000 --- a/requirements/inventoryController/getAllInvInProject.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ❌ Returns 200 if successfully fetch inventory data - - > ## Negative case - -3. ❌ Returns error 404 if the API does not exist -4. ❌ Returns error code 403 if the user is not authorized to view the inventory data -5. ❌ Returns error code 404 if an error occurs when populating or saving. - -> ## Edge case diff --git a/requirements/inventoryController/getAllInvInProjectWBS.md b/requirements/inventoryController/getAllInvInProjectWBS.md deleted file mode 100644 index 16eb0c22f..000000000 --- a/requirements/inventoryController/getAllInvInProjectWBS.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns 200 if successfully found data - -> ## Negative case - -1. ❌ Returns error 404 if the API does not exist -2. ✅ Returns 403 if user is not authorized to view inventory data -3. ✅ Returns 404 if an error occurs while fetching data - -> ## Edge case diff --git a/requirements/inventoryController/postInvInProjectWBS.md b/requirements/inventoryController/postInvInProjectWBS.md deleted file mode 100644 index bd863f9d7..000000000 --- a/requirements/inventoryController/postInvInProjectWBS.md +++ /dev/null @@ -1,19 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post Badge - -> ## Positive case - -1. ❌ Receives a POST request in the **/api/userProfile** route -2. ✅ Returns status code 201, if the inventory was successfully created and saved -3. ✅ Returns status code 201, if the inventory item was succesfully updated and saved. - -> ## Negative case - -1. ❌ Returns error 404 if the API does not exist -2. ✅ Returns error 403 if the user is not authorized to view data -3. ✅ Returns error 500 if an error occurs when saving -4. ✅ Returns error 400 if a valid project was found but quantity and type id were missing - -> ## Edge case diff --git a/requirements/logincontroller/getUser-usecase.md b/requirements/logincontroller/getUser-usecase.md new file mode 100644 index 000000000..aa1dfd50c --- /dev/null +++ b/requirements/logincontroller/getUser-usecase.md @@ -0,0 +1,9 @@ +Check mark: ✅ +Cross Mark: ❌ + +# GetUser + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns **200**, with the requestor body diff --git a/requirements/logincontroller/login-usecase.md b/requirements/logincontroller/login-usecase.md new file mode 100644 index 000000000..6c0188caf --- /dev/null +++ b/requirements/logincontroller/login-usecase.md @@ -0,0 +1,21 @@ +Check mark: ✅ +Cross Mark: ❌ + +# login + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200, if the user is a new user and there is a password match +3. ✅ Returns 200, if the user already exists and the password is a match + +## Negative case + +1. ✅ Returns error 400 if there is no email or password +2. ✅ Returns error 403 if there is no user +3. ✅ Returns error 403 if the user exists but is not active +4. ✅ Returns error 403 if the password is not a match and if the user already exists - in progress + +## Edge case + +1. ✅ Returns the error if the try block fails - in progress \ No newline at end of file diff --git a/requirements/mapLocationsController/deleteLocation.md b/requirements/mapLocationsController/deleteLocation.md deleted file mode 100644 index bbe2bf94b..000000000 --- a/requirements/mapLocationsController/deleteLocation.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get User Profiles - -> ## Positive case - -1. ✅ Receives a GET request in the **/api/userProfile** route -2. ✅ Returns 200 if all is successful - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns 403 if user is not authorized. -3. ✅ Returns 500 if an error occurs when deleting the map location. - -> ## Edge case diff --git a/requirements/mapLocationsController/getAllLocations.md b/requirements/mapLocationsController/getAllLocations.md deleted file mode 100644 index e5edb3434..000000000 --- a/requirements/mapLocationsController/getAllLocations.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get User Profiles - -> ## Positive case - -1. ✅ Receives a GET request in the **/api/userProfile** route -2. ✅ Returns 200 if all is successful - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns 404 if an error occurs when finding all users. -3. ✅ Returns 404 if an error occurs when finding all map locations. - -> ## Edge case diff --git a/requirements/mapLocationsController/putUserLocation.md b/requirements/mapLocationsController/putUserLocation.md deleted file mode 100644 index a68706060..000000000 --- a/requirements/mapLocationsController/putUserLocation.md +++ /dev/null @@ -1,17 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get User Profiles - -> ## Positive case - -1. ✅ Receives a GET request in the **/api/userProfile** route -2. ✅ Returns 200 if all is successful - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns 403 if user is not authorized. -3. ✅ Returns 500 if an error occurs when saving the map location. - -> ## Edge case diff --git a/requirements/mapLocationsController/updateUserLocation.md b/requirements/mapLocationsController/updateUserLocation.md deleted file mode 100644 index cf2de2f2e..000000000 --- a/requirements/mapLocationsController/updateUserLocation.md +++ /dev/null @@ -1,19 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get User Profiles - -> ## Positive case - -1. ✅ Receives a GET request in the **/api/userProfile** route -2. ✅ Returns 200 if all is successful when `userType` is user and clears and resets cache. -3. ✅ Returns 200 if all is successful when `userType` is _not_ user. - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns 403 if user is not authorized. -3. ✅ Returns 500 if an error occurs when updating the user location. -4. ✅ Returns 500 if an error occurs when updating the map location. - -> ## Edge case diff --git a/requirements/mouseoverTextController/createMouseoverText.md b/requirements/mouseoverTextController/createMouseoverText.md new file mode 100644 index 000000000..118803ae8 --- /dev/null +++ b/requirements/mouseoverTextController/createMouseoverText.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createMouseoverText + +> ## Positive case +1. ✅ Return 201 if create new mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 500 if any error when saving the new mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/getMouseoverText.md b/requirements/mouseoverTextController/getMouseoverText.md new file mode 100644 index 000000000..aee52b346 --- /dev/null +++ b/requirements/mouseoverTextController/getMouseoverText.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getMouseoverText + +> ## Positive case +1. ✅ Return 200 if find mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 404 if any error when finding the mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/updateMouseoverText.md b/requirements/mouseoverTextController/updateMouseoverText.md new file mode 100644 index 000000000..84f786c26 --- /dev/null +++ b/requirements/mouseoverTextController/updateMouseoverText.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateMouseoverText + +> ## Positive case +1. ✅ Return 201 if updating mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 500 if any error when finding the mouseoverText by Id +2. ✅ Returns error 400 if any error when saving the mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/notificationController/creatUserNotification.md b/requirements/notificationController/creatUserNotification.md new file mode 100644 index 000000000..bbe81bee8 --- /dev/null +++ b/requirements/notificationController/creatUserNotification.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# Create User Notification + +## Negative Cases + +1. ✅ Returns error 403 if requestor role is not Admin or Owner +2. ✅ Returns error 400 if message and recipient are missing from request +3. ✅ Returns error 500 if there is an internal error while fetching unread notifications. + +## Positive Cases + +1. ✅ Returns status 200 when notification is successfully created with sender, recipient and message diff --git a/requirements/notificationController/deleteUserNotification.md b/requirements/notificationController/deleteUserNotification.md new file mode 100644 index 000000000..f9cdabb80 --- /dev/null +++ b/requirements/notificationController/deleteUserNotification.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# Delete User Notification + +## Negative Cases + +1. ✅ Returns error 403 if requestor role is not Admin or Owner. +2. ✅ Returns error 500 if there is an internal error while deleting notification. + +## Positive Cases + +1. ✅ Returns status 200 when notification is successfully deleted. diff --git a/requirements/notificationController/getSentNotifications.md b/requirements/notificationController/getSentNotifications.md new file mode 100644 index 000000000..c9d541fe7 --- /dev/null +++ b/requirements/notificationController/getSentNotifications.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# GET Sent Notifications + +## Positive Cases + +1. ✅ Returns status 200 Successful Data Retrieval + +## Negative Cases + +1. ✅ Returns error 403 if requestor role is not Admin or Owner +2. ✅ Returns error 500 if there is an internal error while fetching notifications. diff --git a/requirements/notificationController/getUnreadUserNotifications.md b/requirements/notificationController/getUnreadUserNotifications.md new file mode 100644 index 000000000..a3321da76 --- /dev/null +++ b/requirements/notificationController/getUnreadUserNotifications.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# GET Unread User Notifications + +## Negative Cases + +1. ✅ Returns error 403 if userId does not match requestorId. +2. ✅ Returns error 400 if the userId is missing from the request. +3. ✅ Returns error 500 if there is an internal error while fetching unread notifications. + +## Positive Cases + +1. ✅ Returns status 200 with notification data when a valid userId is provided by an Administrator or Owner querying another user's notifications. diff --git a/requirements/notificationController/getUserNotifications.md b/requirements/notificationController/getUserNotifications.md new file mode 100644 index 000000000..d16eba504 --- /dev/null +++ b/requirements/notificationController/getUserNotifications.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# GET User Notifications + +## Negative Cases + +1. ✅ Returns error 403 if userId does not match requestorId. +2. ✅ Returns error 400 if the userId is missing from the request. +3. ✅ Returns error 500 if there is an internal error while fetching notifications. + +## Positive Cases + +1. ✅ Returns status 200 with notification data when a valid userId is provided by an Administrator or Owner querying another user's notifications. diff --git a/requirements/notificationController/markNotificationAsRead.md b/requirements/notificationController/markNotificationAsRead.md new file mode 100644 index 000000000..3971e9086 --- /dev/null +++ b/requirements/notificationController/markNotificationAsRead.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + + +# Mark Notification as Read + +## Negative Cases + +1. ✅ Returns error 400 if recipientId is missing. +2. ✅ Returns error 500 if there is an internal error while reading notification. + +## Positive Cases + +1. ✅ Returns status 200 when notification is successfully read. diff --git a/requirements/ownerMessageController/deleteOwnerMessage.md b/requirements/ownerMessageController/deleteOwnerMessage.md new file mode 100644 index 000000000..1814f55f2 --- /dev/null +++ b/requirements/ownerMessageController/deleteOwnerMessage.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# update Owner Messages + +> ## Negative Cases + +1. ✅ Returns error status 500 if an error occurs during the delete +2. ✅ Returns error status 403 if requestor is not an owner + +> ## Positive Cases + +1. ✅ Returns status 200 and deletes the owner message correctly diff --git a/requirements/ownerMessageController/getOwnerMessage.md b/requirements/ownerMessageController/getOwnerMessage.md new file mode 100644 index 000000000..0d2bd1bc1 --- /dev/null +++ b/requirements/ownerMessageController/getOwnerMessage.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Owner Messages + +> ## Negative Cases + +1. ✅ Returns error status 404 if Owner Message cant be found + + +> ## Positive Cases + +1. ✅ Returns status 200 and initializes a new owner message if none exists. +2. ✅ Returns status 200 and returns the existing owner message if one or more exist. diff --git a/requirements/ownerMessageController/updateOwnerMessage.md b/requirements/ownerMessageController/updateOwnerMessage.md new file mode 100644 index 000000000..1f889e681 --- /dev/null +++ b/requirements/ownerMessageController/updateOwnerMessage.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# update Owner Messages + +> ## Negative Cases + +1. ✅ Returns error status 500 if an error occurs during the update +2. ✅ Returns error status 403 if requestor is not an owner + +> ## Positive Cases + +1. ❌ Returns status 201 and updates the owner message correctly with new message diff --git a/requirements/rolePresetsController/createNewPresets.md b/requirements/rolePresetsController/createNewPresets.md new file mode 100644 index 000000000..7a6edc948 --- /dev/null +++ b/requirements/rolePresetsController/createNewPresets.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createNewPreset + +> ## Positive case +1. ✅ Receives a POST request in the **/api/rolePreset** route +2. ✅ Return 201 if create New Presets successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if missing presetName +3. ✅ Returns 400 if missing roleName +4. ✅ Returns 400 if missing premissions +5. ✅ Returns error 400 when saving new presets + +> ## Edge case diff --git a/requirements/rolePresetsController/deletePresetById.md b/requirements/rolePresetsController/deletePresetById.md new file mode 100644 index 000000000..7698663fb --- /dev/null +++ b/requirements/rolePresetsController/deletePresetById.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deletePresetById + +> ## Positive case + +1. ✅ Return 200 if removing preset by id successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if error in finding by id +3. ✅ Returns 400 if the route doesn't exist +4. ✅ Returns 400 if any error when removing results + +> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/getPresetsByRole.md b/requirements/rolePresetsController/getPresetsByRole.md new file mode 100644 index 000000000..7f14b828e --- /dev/null +++ b/requirements/rolePresetsController/getPresetsByRole.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getPresetsByRole + +> ## Positive case +1. ✅ Receives a GET request in the **/api/rolePreset** route +2. ✅ Return 200 if get Presets by roleName successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 when catching any error in finding roleName + +> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/updatePresetById.md b/requirements/rolePresetsController/updatePresetById.md new file mode 100644 index 000000000..6ce963cf3 --- /dev/null +++ b/requirements/rolePresetsController/updatePresetById.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updatePresetById + +> ## Positive case + +1. ✅ Return 200 if update preset by id successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if the router doesn't exist +3. ✅ Returns 400 if error in finding by id +3. ✅ Returns 400 if any error when saving results + +> ## Edge case \ No newline at end of file diff --git a/requirements/rolesController/createNewRole.md b/requirements/rolesController/createNewRole.md new file mode 100644 index 000000000..391bff025 --- /dev/null +++ b/requirements/rolesController/createNewRole.md @@ -0,0 +1,24 @@ +Check mark: ✅ +Cross Mark: ❌ + +## createNewRole Function + +> ### Positive case +1. ✅ Should return 201 and the new role on success + - Receives a POST request + - User has permission + - Mandatory fields are provided + - Successfully saves the new role to the database + +> ### Negative case +2. ✅ Should return 403 if user lacks permission + - Receives a POST request + - User does not have permission +3. ✅ Should return 400 if mandatory fields are missing + - Receives a POST request + - User has permission + - Mandatory fields are not provided +4. ✅ Should return 500 on role save error + - Receives a POST request + - User has permission + - Error occurs while saving the new role to the database diff --git a/requirements/rolesController/deleteRoleById.md b/requirements/rolesController/deleteRoleById.md new file mode 100644 index 000000000..d7605d1c0 --- /dev/null +++ b/requirements/rolesController/deleteRoleById.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +## deleteRoleById Function + +> ### Positive case +1. ✅ Should return 200 and the deleted role on success + - Receives a DELETE request + - User has permission + - Successfully deletes the role from the database + +> ### Negative case +2. ✅ Should return 403 if user lacks permission + - Receives a DELETE request + - User does not have permission diff --git a/requirements/rolesController/getAllRoles.md b/requirements/rolesController/getAllRoles.md new file mode 100644 index 000000000..a6acc35d5 --- /dev/null +++ b/requirements/rolesController/getAllRoles.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +## getAllRoles Function + +> ### Positive case +1. ✅ Should return 200 and roles on success + + +> ### Negative case +1. ✅ Should return 404 on error when error occurs while retrieving roles from the database diff --git a/requirements/rolesController/getRolesById.md b/requirements/rolesController/getRolesById.md new file mode 100644 index 000000000..6ce3ac123 --- /dev/null +++ b/requirements/rolesController/getRolesById.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +## getRoleById Function + +> ### Positive case +1. ✅ Should return 200 and the role on success + - Receives a GET request + - Successfully retrieves the role by ID from the database + +> ### Negative case +2. ✅ Should return 404 on error + - Receives a GET request + - Error occurs while retrieving the role by ID from the database diff --git a/requirements/rolesController/updateRoleById.md b/requirements/rolesController/updateRoleById.md new file mode 100644 index 000000000..c38a04ecf --- /dev/null +++ b/requirements/rolesController/updateRoleById.md @@ -0,0 +1,28 @@ +Check mark: ✅ +Cross Mark: ❌ + +## updateRoleById Function + +> ### Positive case +1. ✅ Should return 201 and the updated role on success + - Receives a PUT request + - User has permission + - Mandatory fields are provided + - Successfully updates the role in the database + +> ### Negative case +2. ✅ Should return 403 if user lacks permission + - Receives a PUT request + - User does not have permission +3. ✅ Should return 400 if mandatory fields are missing + - Receives a PUT request + - User has permission + - Mandatory fields are not provided +4. ✅ Should return 400 if no valid records are found + - Receives a PUT request + - User has permission + - No valid records are found to update +5. ✅ Should return 500 on role save error + - Receives a PUT request + - User has permission + - Error occurs while saving the updated role to the database diff --git a/requirements/timeOffRequestController/deleteTimeOffRequestById.md b/requirements/timeOffRequestController/deleteTimeOffRequestById.md new file mode 100644 index 000000000..5e832c81d --- /dev/null +++ b/requirements/timeOffRequestController/deleteTimeOffRequestById.md @@ -0,0 +1,21 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Delete Time Off Request By Id + +> ## Positive case + +1. ✅ Returns 200 on successfully deleting the request. + +> ## Negative case + +1. ✅ Returns 403 if the delete request is made my a user for whom all of the below cases are true: + a. User does not have the role of Owner nor of Administrator. + b. User is attempting to delete someone else's timeOffRequest. + c. User does not have the 'manageTimeOffRequests' permission. + +2. ✅ Returns 404 if the timeOffRequest is not found. + +3. ✅ Returns 500 if the some any occured while deleting or checking for permission or any other case. + +> ## Edge case diff --git a/requirements/timeOffRequestController/getTimeOffRequestById.md b/requirements/timeOffRequestController/getTimeOffRequestById.md new file mode 100644 index 000000000..633fed63f --- /dev/null +++ b/requirements/timeOffRequestController/getTimeOffRequestById.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Time Off Request By Id + +> ## Positive case + +1. ✅ Returns all time-off request and status code 200 if successful. + +> ## Negative case + +1. ✅ Return status code 500, if any error is encountered while fetching time-off request using an Id. +2. ✅ Return status code 404, if no time-off request exists with the requested Id. + +> ## Edge case diff --git a/requirements/timeOffRequestController/getTimeOffRequests.md b/requirements/timeOffRequestController/getTimeOffRequests.md new file mode 100644 index 000000000..dd54d5e69 --- /dev/null +++ b/requirements/timeOffRequestController/getTimeOffRequests.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Time Off Requests + +> ## Positive case + +1. ✅ Returns formatted all time-off requests and status code 200 if successful. +2. + +> ## Negative case + +1. ✅ Return status code 500, if any error is encountered while fetching all time-off requests. +2. + +> ## Edge case diff --git a/requirements/timeOffRequestController/setTimeOffRequest.md b/requirements/timeOffRequestController/setTimeOffRequest.md new file mode 100644 index 000000000..4f6e6c81e --- /dev/null +++ b/requirements/timeOffRequestController/setTimeOffRequest.md @@ -0,0 +1,20 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Time Off Requests + +> ## Positive case + +1. ✅ Returns status code 201, if the new time-off request is saved successfully. + +> ## Negative case + +1. ✅ Return status code 403, if the user is not authorized to set time-off request. +2. ✅ Return status code 400, if the request is missing any of the following parameters: + a. duration + b. startingDate + c. reason + d. requestFor +3. ✅ Return status code 500, if any error occurs while setting the time-off request. + +> ## Edge case diff --git a/requirements/timeOffRequestController/updateTimeOffRequestById.md b/requirements/timeOffRequestController/updateTimeOffRequestById.md new file mode 100644 index 000000000..3e7ef74df --- /dev/null +++ b/requirements/timeOffRequestController/updateTimeOffRequestById.md @@ -0,0 +1,26 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Delete Time Off Request By Id + +> ## Positive case + +1. ✅ Returns 200 if the timeOffRequest is successfully updated + +> ## Negative case + +1. ✅ Returns 403 if the delete request is made my a user for whom all of the below cases are true: + a. User does not have the role of Owner nor of Administrator. + b. User does not have the 'manageTimeOffRequests' permission. + +2. ✅ Returns 400 is request body is contains one of the following parameters incorrect: + a. duration + b. reason + c. startingDate + d. requestId + +3. ✅ Returns 404 if no timeOffRequest is found matching the requestId + +4. ✅ Returns 500 if any error occurs + +> ## Edge case diff --git a/src/app.js b/src/app.js index a6d0abd00..359db89c2 100644 --- a/src/app.js +++ b/src/app.js @@ -3,13 +3,22 @@ const Sentry = require('@sentry/node'); const app = express(); const logger = require('./startup/logger'); +const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler').default; logger.init(); + // The request handler must be the first middleware on the app app.use(Sentry.Handlers.requestHandler()); + require('./startup/cors')(app); require('./startup/bodyParser')(app); require('./startup/middleware')(app); require('./startup/routes')(app); -module.exports = { app, logger, Sentry }; +// The error handler must be before any other error middleware and after all controllers +app.use(Sentry.Handlers.errorHandler()); + +// Make it the last middleware since it returns a response and do not call next() +app.use(globalErrorHandler); + +module.exports = { app, logger }; diff --git a/src/controllers/BlueSquareEmailAssignmentController.js b/src/controllers/BlueSquareEmailAssignmentController.js new file mode 100644 index 000000000..429e4b8ef --- /dev/null +++ b/src/controllers/BlueSquareEmailAssignmentController.js @@ -0,0 +1,67 @@ +const BlueSquareEmailAssignmentController = function (BlueSquareEmailAssignment, userProfile) { + const getBlueSquareEmailAssignment = async function (req, res) { + try { + const assignments = await BlueSquareEmailAssignment.find().populate('assignedTo').exec() + res.status(200).send(assignments); + } catch (error) { + console.log(error) + res.status(500).send(error); + } + }; + + const setBlueSquareEmailAssignment = async function (req, res) { + try { + const { email } = req.body; + + if (!email) { + res.status(400).send('bad request'); + return; + } + + const user = await userProfile.findOne({ email }); + if (!userProfile) { + return res.status(400).send('User profile not found'); + } + + const newAssignment = new BlueSquareEmailAssignment({ + email, + assignedTo: user._id, + }); + await newAssignment.save(); + const assignment = await BlueSquareEmailAssignment.find({email}).populate('assignedTo').exec() + + res.status(200).send(assignment[0]); + } catch (error) { + res.status(500).send(error); + } + }; + + const deleteBlueSquareEmailAssignment = async function (req, res) { + try { + const { id } = req.params; + + if (!id) { + res.status(400).send('bad request'); + return; + } + + const deletedAssignment = await BlueSquareEmailAssignment.findOneAndDelete({ _id: id }); + if (!deletedAssignment) { + res.status(404).send('Assignment not found'); + return; + } + + res.status(200).send({id}); + } catch (error) { + res.status(500).send(error); + } + }; + + return { + getBlueSquareEmailAssignment, + setBlueSquareEmailAssignment, + deleteBlueSquareEmailAssignment, + }; +}; + +module.exports = BlueSquareEmailAssignmentController; diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 14c72c76f..55c661b2c 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -3,6 +3,7 @@ const UserProfile = require('../models/userProfile'); const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); const cacheClosure = require('../utilities/nodeCache'); +// const userHelper = require('../helpers/userHelper')(); const badgeController = function (Badge) { /** @@ -12,11 +13,27 @@ const badgeController = function (Badge) { */ const cache = cacheClosure(); + // const awardBadgesTest = async function (req, res) { + // await userHelper.awardNewBadges(); + // res.status(200).send('Badges awarded'); + // }; + const getAllBadges = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'seeBadges')) && !(await helper.hasPermission(req.body.requestor, 'assignBadges'))) { + console.log(req.body.requestor); // Retain logging from development branch for debugging + + // Check if the user has any of the following permissions + if ( + !(await helper.hasPermission(req.body.requestor, 'seeBadges')) && + !(await helper.hasPermission(req.body.requestor, 'assignBadges')) && + !(await helper.hasPermission(req.body.requestor, 'createBadges')) && + !(await helper.hasPermission(req.body.requestor, 'updateBadges')) && + !(await helper.hasPermission(req.body.requestor, 'deleteBadges')) + ) { + console.log('in if statement'); // Retain logging from development branch for debugging res.status(403).send('You are not authorized to view all badge data.'); return; } + // Add cache to reduce database query and optimize performance if (cache.hasCache('allBadges')) { res.status(200).send(cache.getCache('allBadges')); @@ -39,7 +56,7 @@ const badgeController = function (Badge) { cache.setCache('allBadges', results); res.status(200).send(results); }) - .catch(error => res.status(500).send(error)); + .catch((error) => res.status(500).send(error)); }; /** @@ -70,6 +87,13 @@ const badgeController = function (Badge) { res.status(400).send('Can not find the user to be assigned.'); return; } + let totalNewBadges = 0; + const existingBadges = {}; + if (record.badgeCollection && Array.isArray(record.badgeCollection)) { + record.badgeCollection.forEach(badgeItem => { + existingBadges[badgeItem.badge] = badgeItem.count; + }); + } const badgeGroups = req.body.badgeCollection.reduce((grouped, item) => { const { badge } = item; @@ -85,6 +109,7 @@ const badgeController = function (Badge) { return grouped; } + if (!grouped[badge]) { // If the badge is not in the grouped object, add a new entry grouped[badge] = { @@ -112,6 +137,11 @@ const badgeController = function (Badge) { ); } } + if (existingBadges[badge]) { + totalNewBadges += Math.max(0, item.count - existingBadges[badge]); + } else { + totalNewBadges += item.count; + } return grouped; }, {}); @@ -126,6 +156,7 @@ const badgeController = function (Badge) { })); record.badgeCollection = badgeGroupsArray; + record.badgeCount += totalNewBadges; if (cache.hasCache(`user-${userToBeAssigned}`)) { cache.removeCache(`user-${userToBeAssigned}`); @@ -262,14 +293,68 @@ const badgeController = function (Badge) { res.status(200).send({ message: 'Badge successfully updated' }); }); }; + const getBadgeCount = async function (req, res) { + const userId = mongoose.Types.ObjectId(req.params.userId); + + UserProfile.findById(userId, (error, record) => { + // Check for errors or if user profile doesn't exist + if (error || record === null) { + res.sendStatus(404).send('Can not find the user to be assigned.'); + return; + } + // Return badge count from user profile + res.status(200).send({ count: record.badgeCount }); + }); + } + + + const putBadgecount = async function (req, res) { + const userId = mongoose.Types.ObjectId(req.params.userId); + + UserProfile.findById(userId, (error, record) => { + if (error || record === null) { + res.status(400).send('Can not find the user to be assigned.'); + return; + } + record.badgeCount = 1; + + record + .save() + .then(results => res.status(201).send(results._id)) + .catch((err) => { + res.status(500).send(err); + }); + }); + }; + + const resetBadgecount = async function (req, res) { + const userId = mongoose.Types.ObjectId(req.params.userId); + + UserProfile.findById(userId, (error, record) => { + if (error || record === null) { + res.status(400).send('Can not find the user to be assigned.'); + return; + } + record.badgeCount = 0; + + record.save(); + res.status(201).send({ count: record.badgeCount }); + + }); + } + return { + // awardBadgesTest, getAllBadges, assignBadges, postBadge, deleteBadge, putBadge, + getBadgeCount, + putBadgecount, + resetBadgecount }; }; -module.exports = badgeController; +module.exports = badgeController; \ No newline at end of file diff --git a/src/controllers/badgeController.spec.js b/src/controllers/badgeController.spec.js index 0149bee49..3c0c92b7f 100644 --- a/src/controllers/badgeController.spec.js +++ b/src/controllers/badgeController.spec.js @@ -1,6 +1,7 @@ -// const mongoose = require('mongoose'); -// const UserProfile = require('../models/userProfile'); const mongoose = require('mongoose'); +// mock the cache function before importing so we can manipulate the implementation +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); const Badge = require('../models/badge'); const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); @@ -8,23 +9,17 @@ const badgeController = require('./badgeController'); const { mockReq, mockRes, assertResMock } = require('../test'); const UserProfile = require('../models/userProfile'); -// mock the cache function before importing so we can manipulate the implementation -jest.mock('../utilities/nodeCache'); -const cache = require('../utilities/nodeCache'); - const makeSut = () => { - const { postBadge, getAllBadges, assignBadges, deleteBadge, putBadge } = badgeController(Badge); + const { postBadge, getAllBadges, assignBadges, deleteBadge } = badgeController(Badge); - return { postBadge, getAllBadges, assignBadges, deleteBadge, putBadge }; + return { postBadge, getAllBadges, assignBadges, deleteBadge }; }; -// Allows us to test functions using promise chaining. const flushPromises = () => new Promise(setImmediate); const mockHasPermission = (value) => jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); -// eslint-disable-next-line no-unused-vars const makeMockCache = (method, value) => { const cacheObject = { getCache: jest.fn(), @@ -170,83 +165,81 @@ describe('badeController module', () => { assertResMock(500, new Error(errorMsg), response, mockRes); }); - // test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { - // const { mockCache: getCacheMock } = makeMockCache('getCache', ''); - // const { postBadge } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - - // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - // const newBadge = { - // badgeName: mockReq.body.badgeName, - // category: mockReq.body.category, - // multiple: mockReq.body.multiple, - // totalHrs: mockReq.body.totalHrs, - // weeks: mockReq.body.weeks, - // months: mockReq.body.months, - // people: mockReq.body.people, - // project: mockReq.body.project, - // imageUrl: mockReq.body.imageUrl, - // ranking: mockReq.body.ranking, - // description: mockReq.body.description, - // showReport: mockReq.body.showReport, - // }; - - // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - // const response = await postBadge(mockReq, mockRes); - - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - // expect(findSpy).toHaveBeenCalledWith({ - // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - // }); - // assertResMock(201, newBadge, response, mockRes); - // }); - - // test('Clears cache if all is successful and there is a badge cache', async () => { - // const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); - // const removeCacheMock = jest - // .spyOn(cacheObject, 'removeCache') - // .mockImplementationOnce(() => null); - // const { postBadge } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - - // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - // const newBadge = { - // badgeName: mockReq.body.badgeName, - // category: mockReq.body.category, - // multiple: mockReq.body.multiple, - // totalHrs: mockReq.body.totalHrs, - // weeks: mockReq.body.weeks, - // months: mockReq.body.months, - // people: mockReq.body.people, - // project: mockReq.body.project, - // imageUrl: mockReq.body.imageUrl, - // ranking: mockReq.body.ranking, - // description: mockReq.body.description, - // showReport: mockReq.body.showReport, - // }; - - // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - // const response = await postBadge(mockReq, mockRes); - - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - // expect(findSpy).toHaveBeenCalledWith({ - // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - // }); - // assertResMock(201, newBadge, response, mockRes); - // }); + test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { + const { mockCache: getCacheMock } = makeMockCache('getCache', ''); + const { postBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + const newBadge = { + badgeName: mockReq.body.badgeName, + category: mockReq.body.category, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + weeks: mockReq.body.weeks, + months: mockReq.body.months, + people: mockReq.body.people, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl, + ranking: mockReq.body.ranking, + description: mockReq.body.description, + showReport: mockReq.body.showReport, + }; + + jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + const response = await postBadge(mockReq, mockRes); + + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + expect(findSpy).toHaveBeenCalledWith({ + badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + }); + assertResMock(201, newBadge, response, mockRes); + }); + + test('Clears cache if all is successful and there is a badge cache', async () => { + const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const { postBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + const newBadge = { + badgeName: mockReq.body.badgeName, + category: mockReq.body.category, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + weeks: mockReq.body.weeks, + months: mockReq.body.months, + people: mockReq.body.people, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl, + ranking: mockReq.body.ranking, + description: mockReq.body.description, + showReport: mockReq.body.showReport, + }; + + jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + const response = await postBadge(mockReq, mockRes); + + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + expect(findSpy).toHaveBeenCalledWith({ + badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + }); + assertResMock(201, newBadge, response, mockRes); + }); }); describe('getAllBadges method', () => { - // eslint-disable-next-line no-unused-vars const findObject = { populate: () => {} }; - // eslint-disable-next-line no-unused-vars const populateObject = { sort: () => {} }; test('Returns 403 if the user is not authorized', async () => { const { getAllBadges } = makeSut(); @@ -260,93 +253,93 @@ describe('badeController module', () => { expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); }); - // test('Returns 500 if an error occurs when querying DB', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { getAllBadges } = makeSut(); - // const mockPermission = mockHasPermission(true); - // const errorMsg = 'Error when finding badges'; - - // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - // const populateMock = jest - // .spyOn(findObject, 'populate') - // .mockImplementationOnce(() => populateObject); - // const sortMock = jest - // .spyOn(populateObject, 'sort') - // .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); - - // getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockRes.status).toHaveBeenCalledWith(500); - // expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // expect(findMock).toHaveBeenCalledWith( - // {}, - // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - // ); - // expect(populateMock).toHaveBeenCalledWith({ - // path: 'project', - // select: '_id projectName', - // }); - // expect(sortMock).toHaveBeenCalledWith({ - // ranking: 1, - // badgeName: 1, - // }); - // }); - - // test('Returns 200 if the badges are in cache', async () => { - // const badges = [{ badge: 'random badge' }]; - // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - // const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); - - // const { getAllBadges } = makeSut(); - - // const mockPermission = mockHasPermission(true); - - // const response = await getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // assertResMock(200, badges, response, mockRes); - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // }); - - // test('Returns 200 if not in cache, and all the async code succeeds.', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { getAllBadges } = makeSut(); - // const mockPermission = mockHasPermission(true); - // const badges = [{ badge: 'random badge' }]; - - // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - // const populateMock = jest - // .spyOn(findObject, 'populate') - // .mockImplementationOnce(() => populateObject); - // const sortMock = jest - // .spyOn(populateObject, 'sort') - // .mockImplementationOnce(() => Promise.resolve(badges)); - - // getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockRes.status).toHaveBeenCalledWith(200); - // expect(mockRes.send).toHaveBeenCalledWith(badges); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // expect(findMock).toHaveBeenCalledWith( - // {}, - // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - // ); - // expect(populateMock).toHaveBeenCalledWith({ - // path: 'project', - // select: '_id projectName', - // }); - // expect(sortMock).toHaveBeenCalledWith({ - // ranking: 1, - // badgeName: 1, - // }); - // }); + test('Returns 500 if an error occurs when querying DB', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + const { getAllBadges } = makeSut(); + const mockPermission = mockHasPermission(true); + const errorMsg = 'Error when finding badges'; + + const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + const populateMock = jest + .spyOn(findObject, 'populate') + .mockImplementationOnce(() => populateObject); + const sortMock = jest + .spyOn(populateObject, 'sort') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + + getAllBadges(mockReq, mockRes); + await flushPromises(); + + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + expect(findMock).toHaveBeenCalledWith( + {}, + 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + ); + expect(populateMock).toHaveBeenCalledWith({ + path: 'project', + select: '_id projectName', + }); + expect(sortMock).toHaveBeenCalledWith({ + ranking: 1, + badgeName: 1, + }); + }); + + test('Returns 200 if the badges are in cache', async () => { + const badges = [{ badge: 'random badge' }]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); + + const { getAllBadges } = makeSut(); + + const mockPermission = mockHasPermission(true); + + const response = await getAllBadges(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, badges, response, mockRes); + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + }); + + test('Returns 200 if not in cache, and all the async code succeeds.', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + const { getAllBadges } = makeSut(); + const mockPermission = mockHasPermission(true); + const badges = [{ badge: 'random badge' }]; + + const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + const populateMock = jest + .spyOn(findObject, 'populate') + .mockImplementationOnce(() => populateObject); + const sortMock = jest + .spyOn(populateObject, 'sort') + .mockImplementationOnce(() => Promise.resolve(badges)); + + getAllBadges(mockReq, mockRes); + await flushPromises(); + + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(badges); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + expect(findMock).toHaveBeenCalledWith( + {}, + 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + ); + expect(populateMock).toHaveBeenCalledWith({ + path: 'project', + select: '_id projectName', + }); + expect(sortMock).toHaveBeenCalledWith({ + ranking: 1, + badgeName: 1, + }); + }); }); describe('assignBadges method', () => { @@ -392,72 +385,72 @@ describe('badeController module', () => { expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); }); - // test('Returns 500 if an error occurs when saving edited user profile', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + test('Returns 500 if an error occurs when saving edited user profile', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { assignBadges } = makeSut(); + const { assignBadges } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - // const errMsg = 'Error when saving'; - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); + const hasPermissionSpy = mockHasPermission(true); + const errMsg = 'Error when saving'; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); - // const response = await assignBadges(mockReq, mockRes); + const response = await assignBadges(mockReq, mockRes); - // assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); + assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); - // test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { - // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - // const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); + test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); - // const { assignBadges } = makeSut(); + const { assignBadges } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + const hasPermissionSpy = mockHasPermission(true); + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); - // const response = await assignBadges(mockReq, mockRes); + const response = await assignBadges(mockReq, mockRes); - // assertResMock(201, `randomId`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); - // expect(removeCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); + assertResMock(201, `randomId`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); + expect(removeCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); - // test('Returns 201 and if successful and user does not exist in cache', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + test('Returns 201 and if successful and user does not exist in cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { assignBadges } = makeSut(); + const { assignBadges } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + const hasPermissionSpy = mockHasPermission(true); + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); - // const response = await assignBadges(mockReq, mockRes); + const response = await assignBadges(mockReq, mockRes); - // assertResMock(201, `randomId`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + assertResMock(201, `randomId`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); }); describe('deleteBadge method', () => { @@ -630,89 +623,4 @@ describe('badeController module', () => { expect(removeCacheSpy).toHaveBeenCalledWith('allBadges'); }); }); - - describe('putBadge method', () => { - test('Returns 403 if the user is not authorized', async () => { - const { putBadge } = makeSut(); - const hasPermissionSpy = mockHasPermission(false); - - const response = await putBadge(mockReq, mockRes); - await flushPromises(); - - assertResMock(403, { error: 'You are not authorized to update badges.' }, response, mockRes); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); - }); - - test('Returns 400 if an error occurs in findById', async () => { - const { putBadge } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - - const findByIdAndUpdateSpy = jest - .spyOn(Badge, 'findByIdAndUpdate') - .mockImplementationOnce((_, __, cb) => cb(true, true)); - - const response = await putBadge(mockReq, mockRes); - await flushPromises(); - - const data = { - badgeName: mockReq.body.name || mockReq.body.badgeName, - description: mockReq.body.description, - type: mockReq.body.type, - multiple: mockReq.body.multiple, - totalHrs: mockReq.body.totalHrs, - people: mockReq.body.people, - category: mockReq.body.category, - months: mockReq.body.months, - weeks: mockReq.body.weeks, - project: mockReq.body.project, - imageUrl: mockReq.body.imageUrl || mockReq.body.imageURL, - ranking: mockReq.body.ranking, - showReport: mockReq.body.showReport, - }; - - expect(findByIdAndUpdateSpy).toHaveBeenCalledWith( - mockReq.params.badgeId, - data, - expect.anything(), - ); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); - assertResMock(400, { error: 'No valid records found' }, response, mockRes); - }); - - test('Returns 400 if no badge is found', async () => { - const { putBadge } = makeSut(); - const hasPermissionSpy = mockHasPermission(true); - - const findByIdAndUpdateSpy = jest - .spyOn(Badge, 'findByIdAndUpdate') - .mockImplementationOnce((_, __, cb) => cb(false, null)); - - const response = await putBadge(mockReq, mockRes); - await flushPromises(); - - const data = { - badgeName: mockReq.body.name || mockReq.body.badgeName, - description: mockReq.body.description, - type: mockReq.body.type, - multiple: mockReq.body.multiple, - totalHrs: mockReq.body.totalHrs, - people: mockReq.body.people, - category: mockReq.body.category, - months: mockReq.body.months, - weeks: mockReq.body.weeks, - project: mockReq.body.project, - imageUrl: mockReq.body.imageUrl || mockReq.body.imageURL, - ranking: mockReq.body.ranking, - showReport: mockReq.body.showReport, - }; - - expect(findByIdAndUpdateSpy).toHaveBeenCalledWith( - mockReq.params.badgeId, - data, - expect.anything(), - ); - expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'updateBadges'); - assertResMock(400, { error: 'No valid records found' }, response, mockRes); - }); - }); }); diff --git a/src/controllers/bmdashboard/bmEquipmentController.js b/src/controllers/bmdashboard/bmEquipmentController.js index 1255493ca..d3230082e 100644 --- a/src/controllers/bmdashboard/bmEquipmentController.js +++ b/src/controllers/bmdashboard/bmEquipmentController.js @@ -54,6 +54,45 @@ const bmEquipmentController = (BuildingEquipment) => { } }; + const fetchBMEquipments = async (req, res) => { + try { + BuildingEquipment + .find() + .populate([ + { + path: 'project', + select: '_id name', + }, + { + path: 'itemType', + select: '_id name', + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + ]) + .exec() + .then((result) => { + res.status(200).send(result); + }) + .catch((error) => res.status(500).send(error)); + } catch (err) { + res.json(err); + } +}; + + const bmPurchaseEquipments = async function (req, res) { const { projectId, @@ -103,6 +142,7 @@ const bmEquipmentController = (BuildingEquipment) => { return { fetchSingleEquipment, bmPurchaseEquipments, + fetchBMEquipments, }; }; diff --git a/src/controllers/bmdashboard/bmInventoryTypeController.js b/src/controllers/bmdashboard/bmInventoryTypeController.js index f4cd6cf99..175d948b4 100644 --- a/src/controllers/bmdashboard/bmInventoryTypeController.js +++ b/src/controllers/bmdashboard/bmInventoryTypeController.js @@ -32,12 +32,39 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } const fetchToolTypes = async (req, res) => { + try { - ToolType.find() + ToolType + .find() + .populate([ + { + path: 'available', + select: '_id code project', + populate: { + path: 'project', + select: '_id name' + } + }, + { + path: 'using', + select: '_id code project', + populate: { + path: 'project', + select: '_id name' + } + } + ]) .exec() - .then((result) => res.status(200).send(result)) - .catch((error) => res.status(500).send(error)); + .then(result => { + res.status(200).send(result); + }) + .catch(error => { + console.error("fetchToolTypes error: ", error); + res.status(500).send(error); + }); + } catch (err) { + console.log("error: ", err) res.json(err); } }; @@ -174,6 +201,75 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } } + async function addToolType(req, res) { + const { + name, + description, + invoice, + purchaseRental, + fromDate, + toDate, + condition, + phoneNumber, + quantity, + currency, + unitPrice, + shippingFee, + taxes, + totalPriceWithShipping, + images, + link, + requestor: { requestorId }, + } = req.body; + + try { + ToolType.find({ name }) + .then((result) => { + if (result.length) { + res.status(409).send('Oops!! Tool already exists!'); + } else { + const newDoc = { + category: 'Tool', + name, + description, + invoice, + purchaseRental, + fromDate, + toDate, + condition, + phoneNumber, + quantity, + currency, + unitPrice, + shippingFee, + taxes, + totalPriceWithShipping, + images, + link, + createdBy: requestorId, + }; + ToolType.create(newDoc) + .then((results) => { + res.status(201).send(results); + }) + .catch((error) => { + if (error._message.includes('validation failed')) { + res.status(400).send(error.errors.unit.message); + } else { + res.status(500).send(error); + } + }); + } + }) + .catch((error) => { + res.status(500).send(error); + }); + } catch (error) { + res.status(500).send(error); + } + } + + async function fetchInventoryByType(req, res) { const { type } = req.params; let SelectedType = InvType; @@ -245,6 +341,18 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp res.status(500).send(error); } } + + async function fetchEquipmentTypes(req, res) { + try { + EquipType.find() + .exec() + .then((result) => res.status(200).send(result)) + .catch((error) => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + } + const fetchSingleInventoryType = async (req, res) => { const { invtypeId } = req.params; try { @@ -290,10 +398,12 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp fetchReusableTypes, fetchToolTypes, addEquipmentType, + fetchEquipmentTypes, fetchSingleInventoryType, updateNameAndUnit, addMaterialType, addConsumableType, + addToolType, fetchInvUnitsFromJson, fetchInventoryByType, }; diff --git a/src/controllers/bmdashboard/bmReusableController.js b/src/controllers/bmdashboard/bmReusableController.js index e4ae0574f..83cd19475 100644 --- a/src/controllers/bmdashboard/bmReusableController.js +++ b/src/controllers/bmdashboard/bmReusableController.js @@ -3,10 +3,10 @@ const { reusableType: ReusableType, } = require('../../models/bmdashboard/buildingInventoryType'); -function isValidDate(dateString) { - const date = new Date(dateString); - return !isNaN(date.getTime()); -} +// function isValidDate(dateString) { +// const date = new Date(dateString); +// return !isNaN(date.getTime()); +// } const bmReusableController = function (BuildingReusable) { const fetchBMReusables = async (req, res) => { @@ -103,9 +103,154 @@ const bmReusableController = function (BuildingReusable) { } }; + const bmPostReusableUpdateRecord = function (req, res) { + const payload = req.body; + let quantityUsed = +req.body.quantityUsed; + let quantityWasted = +req.body.quantityWasted; + const { reusable } = req.body; + if (payload.QtyUsedLogUnit === "percent" && quantityWasted >= 0) { + quantityUsed = +((+quantityUsed / 100) * reusable.stockAvailable).toFixed( + 4 + ); + } + if (payload.QtyWastedLogUnit === "percent" && quantityUsed >= 0) { + quantityWasted = +( + (+quantityWasted / 100) * + reusable.stockAvailable + ).toFixed(4); + } + + if ( + quantityUsed > reusable.stockAvailable || + quantityWasted > reusable.stockAvailable || + quantityUsed + quantityWasted > reusable.stockAvailable + ) { + res + .status(500) + .send( + "Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available." + ); + } else { + let newStockUsed = +reusable.stockUsed + parseFloat(quantityUsed); + let newStockWasted = +reusable.stockWasted + parseFloat(quantityWasted); + let newAvailable = + +reusable.stockAvailable - + parseFloat(quantityUsed) - + parseFloat(quantityWasted); + newStockUsed = parseFloat(newStockUsed.toFixed(4)); + newStockWasted = parseFloat(newStockWasted.toFixed(4)); + newAvailable = parseFloat(newAvailable.toFixed(4)); + BuildingReusable.updateOne( + { _id: req.body.reusable._id }, + + { + $set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + $push: { + updateRecord: { + date: req.body.date, + createdBy: req.body.requestor.requestorId, + quantityUsed, + quantityWasted, + }, + }, + } + ) + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => res.status(500).send({ message: error })); + } + }; + + const bmPostReusableUpdateBulk = function (req, res) { + const reusableUpdates = req.body.upadateReusables; + let errorFlag = false; + const updateRecordsToBeAdded = []; + for (let i = 0; i < reusableUpdates.length; i+=1) { + const payload = reusableUpdates[i]; + let quantityUsed = +payload.quantityUsed; + let quantityWasted = +payload.quantityWasted; + const { reusable } = payload; + if (payload.QtyUsedLogUnit === "percent" && quantityWasted >= 0) { + quantityUsed = +( + (+quantityUsed / 100) * + reusable.stockAvailable + ).toFixed(4); + } + if (payload.QtyWastedLogUnit === "percent" && quantityUsed >= 0) { + quantityWasted = +( + (+quantityWasted / 100) * + reusable.stockAvailable + ).toFixed(4); + } + + let newStockUsed = +reusable.stockUsed + parseFloat(quantityUsed); + let newStockWasted = +reusable.stockWasted + parseFloat(quantityWasted); + let newAvailable = + +reusable.stockAvailable - + parseFloat(quantityUsed) - + parseFloat(quantityWasted); + newStockUsed = parseFloat(newStockUsed.toFixed(4)); + newStockWasted = parseFloat(newStockWasted.toFixed(4)); + newAvailable = parseFloat(newAvailable.toFixed(4)); + if (newAvailable < 0) { + errorFlag = true; + break; + } + updateRecordsToBeAdded.push({ + updateId: reusable._id, + set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + updateValue: { + createdBy: req.body.requestor.requestorId, + quantityUsed, + quantityWasted, + date: req.body.date, + }, + }); + } + + try { + if (errorFlag) { + res.status(500).send("Stock quantities submitted seems to be invalid"); + return; + } + const updatePromises = updateRecordsToBeAdded.map((updateItem) => + BuildingReusable.updateOne( + { _id: updateItem.updateId }, + { + $set: updateItem.set, + $push: { updateRecord: updateItem.updateValue }, + } + ).exec() + ); + Promise.all(updatePromises) + .then((results) => { + res.status(200).send({ + result: `Successfully posted log for ${results.length} Reusable records.`, + }); + }) + .catch((error) => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + + + + return { fetchBMReusables, purchaseReusable, + bmPostReusableUpdateRecord, + bmPostReusableUpdateBulk, }; }; diff --git a/src/controllers/bmdashboard/bmToolController.js b/src/controllers/bmdashboard/bmToolController.js index c620255b7..be37639ac 100644 --- a/src/controllers/bmdashboard/bmToolController.js +++ b/src/controllers/bmdashboard/bmToolController.js @@ -1,6 +1,61 @@ const mongoose = require('mongoose'); -const bmToolController = (BuildingTool) => { +const bmToolController = (BuildingTool, ToolType) => { + + const fetchAllTools = (req, res) => { + const populateFields = [ + { + path: 'project', + select: '_id name', + }, + { + path: 'itemType', + select: '_id name description unit imageUrl category available using', + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + { + path: 'logRecord', + populate: [ + { + path: 'createdBy', + select: '_id firstName lastName', + }, + { + path: 'responsibleUser', + select: '_id firstName lastName', + }, + ], + }, + ]; + + BuildingTool.find() + .populate(populateFields) + .exec() + .then(results => { + res.status(200).send(results); + }) + .catch(error => { + const errorMessage = `Error occurred while fetching tools: ${error.message}`; + console.error(errorMessage); + res.status(500).send({ message: errorMessage }); + }); + }; + + + const fetchSingleTool = async (req, res) => { const { toolId } = req.params; try { @@ -101,9 +156,93 @@ const bmToolController = (BuildingTool) => { } }; + const bmLogTools = async function (req, res) { + const requestor = req.body.requestor.requestorId; + const {typesArray, action, date} = req.body + const results = []; + const errors = []; + + if(typesArray.length === 0 || typesArray === undefined){ + errors.push({ message: 'Invalid request. No tools selected'}) + return res.status(500).send({errors, results}); + } + + for (const type of typesArray) { + const toolName = type.toolName; + const toolCodes = type.toolCodes; + const codeMap = {}; + toolCodes.forEach(obj => { + codeMap[obj.value] = obj.label; + }) + + try{ + const toolTypeDoc = await ToolType.findOne({ _id: mongoose.Types.ObjectId(type.toolType) }); + if(!toolTypeDoc) { + errors.push({ message: `Tool type ${toolName} with id ${type.toolType} was not found.`}); + continue; + } + const availableItems = toolTypeDoc.available; + const usingItems = toolTypeDoc.using; + + for(const toolItem of type.toolItems){ + const buildingToolDoc = await BuildingTool.findOne({ _id: mongoose.Types.ObjectId(toolItem)}); + if(!buildingToolDoc){ + errors.push({ message: `${toolName} with id ${toolItem} was not found.`}); + continue; + } + + if(action === "Check Out" && availableItems.length > 0){ + const foundIndex = availableItems.indexOf(toolItem); + if(foundIndex >= 0){ + availableItems.splice(foundIndex, 1); + usingItems.push(toolItem); + }else{ + errors.push({ message: `${toolName} with code ${codeMap[toolItem]} is not available for ${action}`}); + continue; + } + } + + if(action === "Check In" && usingItems.length > 0){ + const foundIndex = usingItems.indexOf(toolItem); + if(foundIndex >= 0){ + usingItems.splice(foundIndex, 1); + availableItems.push(toolItem); + }else{ + errors.push({ message: `${toolName} ${codeMap[toolItem]} is not available for ${action}`}); + continue; + } + } + + const newRecord = { + date: date, + createdBy: requestor, + responsibleUser: buildingToolDoc.userResponsible, + type: action + } + + buildingToolDoc.logRecord.push(newRecord); + buildingToolDoc.save(); + results.push({message: `${action} successful for ${toolName} ${codeMap[toolItem]}`}) + } + + await toolTypeDoc.save(); + }catch(error){ + errors.push({message: `Error for tool type ${type}: ${error.message}` }); + } + } + + if (errors.length > 0) { + return res.status(404).send({ errors, results }); + } else { + return res.status(200).send({ errors, results }); + } + } + return { + fetchAllTools, fetchSingleTool, bmPurchaseTools, + bmLogTools }; }; diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index b1dc150f4..455bf3fc2 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -2,12 +2,13 @@ const path = require("path"); const fs = require("fs/promises"); const mongoose = require("mongoose"); -const dashboardhelper = require("../helpers/dashboardhelper")(); +const dashboardHelperClosure = require("../helpers/dashboardhelper"); const emailSender = require("../utilities/emailSender"); const AIPrompt = require("../models/weeklySummaryAIPrompt"); const User = require("../models/userProfile"); const dashboardcontroller = function () { + const dashboardhelper = dashboardHelperClosure(); const dashboarddata = function (req, res) { const userId = mongoose.Types.ObjectId(req.params.userId); @@ -347,3 +348,4 @@ const dashboardcontroller = function () { }; module.exports = dashboardcontroller; + diff --git a/src/controllers/dashBoardController.spec.js b/src/controllers/dashBoardController.spec.js new file mode 100644 index 000000000..b424eafe7 --- /dev/null +++ b/src/controllers/dashBoardController.spec.js @@ -0,0 +1,838 @@ +// const mongoose = require('mongoose'); +const AIPrompt = require('../models/weeklySummaryAIPrompt'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const UserProfile = require('../models/userProfile'); + +jest.mock('../utilities/emailSender'); +const emailSender = require('../utilities/emailSender'); + +jest.mock('../helpers/dashboardhelper'); +const dashboardHelperClosure = require('../helpers/dashboardhelper'); +const dashBoardController = require('./dashBoardController'); + +// mock the cache function before importing so we can manipulate the implementation +// jest.mock('../utilities/nodeCache'); +// const cache = require('../utilities/nodeCache'); +const makeSut = () => { + const { + updateCopiedPrompt, + getPromptCopiedDate, + updateAIPrompt, + getAIPrompt, + monthlydata, + weeklydata, + leaderboarddata, + orgData, + dashboarddata, + sendBugReport, + sendMakeSuggestion, + getSuggestionOption, + editSuggestionOption + } = dashBoardController(AIPrompt); + return { + updateCopiedPrompt, + getPromptCopiedDate, + updateAIPrompt, + getAIPrompt, + monthlydata, + weeklydata, + leaderboarddata, + orgData, + dashboarddata, + sendBugReport, + sendMakeSuggestion, + getSuggestionOption, + editSuggestionOption + }; +}; + + +const flushPromises = async () => new Promise(setImmediate); + +describe('Dashboard Controller tests', () => { + beforeAll(() => { + + }); + beforeEach(() => { + // dashboardhelper = dashboardHelperClosure(); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + const error = new Error('any error'); + + + describe('updateCopiedPrompt Tests', () => { + test('Returns error 500 if the error occurs in the file update function', async () => { + + const { updateCopiedPrompt } = makeSut(); + + jest + .spyOn(UserProfile, 'findOneAndUpdate') + .mockImplementationOnce(() => + Promise.reject(new Error('Error Occured in the findOneAndUpdate function')), + ); + + const response = await updateCopiedPrompt(mockReq, mockRes); + + assertResMock( + 500, + new Error('Error Occured in the findOneAndUpdate function'), + response, + mockRes, + ); + }); + + test('Returns error 404 if the user is not found', async () => { + + const { updateCopiedPrompt } = makeSut(); + + jest. + spyOn(UserProfile, 'findOneAndUpdate') + .mockImplementationOnce(() => + Promise.resolve(null) + ); + + const response = await updateCopiedPrompt(mockReq, mockRes); + + assertResMock( + 404, + { message: "User not found " }, + response, + mockRes, + ); + }); + + test('Returns 200 if there is no error and user is found', async () => { + + const { updateCopiedPrompt } = makeSut(); + + jest + .spyOn(UserProfile, 'findOneAndUpdate') + .mockImplementationOnce(() => + Promise.resolve("Copied AI prompt") + ); + + const response = await updateCopiedPrompt(mockReq, mockRes); + + assertResMock( + 200, + "Copied AI prompt", + response, + mockRes, + ); + }) + + }); + + describe('getPromptCopiedDate', () => { + test('Returns 200 if there is a user and return copied AI prompt',async () => { + const mockUser = { _id: 'testUserId', copiedAiPrompt: 'Test Prompt'}; + + const newReq = { + ...mockReq, + params: { + userId: 'testUserId' + } + }; + + const { getPromptCopiedDate } = makeSut(); + + jest + .spyOn(UserProfile, 'findOne') + .mockResolvedValueOnce(mockUser); + + await getPromptCopiedDate(newReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith({ message: mockUser.copiedAiPrompt }); + }); + + test('Returns undefined when the user is not found', async () => { + + const { getPromptCopiedDate } = makeSut(); + + jest + .spyOn(UserProfile, 'findOne') + .mockResolvedValueOnce(null); + + getPromptCopiedDate(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).not.toHaveBeenCalled(); + expect(mockRes.send).not.toHaveBeenCalled(); + }) + }) + + describe('updateAIPrompt Tests', () => { + test('Returns error 500 if the error occurs in the AI Prompt function', async () => { + const newRequest = { + ...mockReq, + body: { + requestor: { + role: 'Owner' + } + } + }; + + const { updateAIPrompt } = makeSut(); + + jest + .spyOn(AIPrompt, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.reject(error)); + + const response = updateAIPrompt(newRequest, mockRes); + + await flushPromises(); + + assertResMock( + 500, + error, + response, + mockRes, + ); + }); + + test('Returns 200 if there is no error and AI Prompt is saved', async () => { + const newRequest = { + ...mockReq, + body: { + requestor: { + role: 'Owner' + } + } + }; + + const { updateAIPrompt } = makeSut(); + + jest + .spyOn(AIPrompt, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve("Successfully saved AI prompt.")); + + const response = updateAIPrompt(newRequest, mockRes); + + await flushPromises(); + + assertResMock( + 200, + "Successfully saved AI prompt.", + response, + mockRes, + ); + }); + + test('Returns undefined if requestor role is not an owner', () => { + const newRequest = { + ...mockReq, + body: { + requestor: { + role: 'Administrator' + } + } + }; + const { updateAIPrompt } = makeSut(); + + const mockFindOneAndUpdate = jest + .spyOn(AIPrompt, 'findOneAndUpdate') + .mockImplementationOnce(() => + Promise.resolve({undefined}), + ); + + const response = updateAIPrompt(newRequest, mockRes); + + expect(response).toBeUndefined(); + expect(mockRes.status).not.toHaveBeenCalled(); + expect(mockRes.send).not.toHaveBeenCalled(); + expect(mockFindOneAndUpdate).not.toHaveBeenCalled(); + }); + + }); + + describe('getAIPrompt Tests', () => { + + test('Returns 200 if the GPT exists and send the results back', async () => { + + const { getAIPrompt } = makeSut(); + + jest + .spyOn(AIPrompt,'findById') + .mockImplementationOnce(() => Promise.resolve({})) + + const response = getAIPrompt(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + {}, + response, + mockRes, + ) + }); + + test('Returns 200 if there is no error and new GPT Prompt is created', async () => { + + const { getAIPrompt } = makeSut(); + + jest + .spyOn(AIPrompt, 'findById') + .mockResolvedValueOnce(null); + + jest + .spyOn(AIPrompt, 'create') + .mockImplementationOnce(() => Promise.resolve({})); + + const response = getAIPrompt(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + {}, + response, + mockRes, + ) + }); + + test('Returns 500 if GPT Prompt does not exist', async () => { + + const { getAIPrompt } = makeSut(); + const errorMessage = 'GPT Prompt does not exist'; + + jest + .spyOn(AIPrompt, 'findById') + .mockRejectedValueOnce(new Error(errorMessage)); + + const response = getAIPrompt(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 500, + new Error(errorMessage), + response, + mockRes, + ); + }); + + test('Returns 500 if there is an error in creating the GPT Prompt', async () => { + + const { getAIPrompt } = makeSut(); + const errorMessage = 'Error in creating the GPT Prompt'; + + jest + .spyOn(AIPrompt, 'findById') + .mockResolvedValueOnce(null); + + jest + .spyOn(AIPrompt, 'create') + .mockRejectedValueOnce(new Error(errorMessage)); + + const response = getAIPrompt(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 500, + new Error(errorMessage), + response, + mockRes, + ); + }); + + }); + + describe('weeklydata Tests', () => { + + test('Returns 200 if there is no error and labordata is found', async () => { + const dashboardHelperObject = + { + laborthisweek: jest.fn(() => Promise.resolve([])) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { weeklydata } = makeSut(); + + const response = weeklydata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + [], + response, + mockRes, + ); + }) + }); + + describe('monthlydata Tests', () => { + + test('Returns 200 if there is no results and return empty results', async () => { + const dashboardHelperObject = { + laborthismonth: jest.fn(() => Promise.resolve([{ + projectName: "", + timeSpent_hrs: 0, + }])) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { monthlydata } = makeSut(); + + const response = monthlydata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + [{ + projectName: "", + timeSpent_hrs: 0, + }], + response, + mockRes, + ); + }) + + test('Returns 200 if there is results and return results', async () => { + const dashboardHelperObject = { + laborthismonth: jest.fn(() => Promise.resolve({})) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { monthlydata } = makeSut(); + + const response = monthlydata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + {}, + response, + mockRes, + ); + }) + + }); + + describe('leaderboarddata Tests', () => { + test('Returns 200 if there is leaderboard data', async () => { + const dashboardHelperObject = { + getLeaderboard: jest.fn(() => Promise.resolve({})), + getUserLaborData: jest.fn(() => Promise.resolve({})) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { leaderboarddata } = makeSut(); + + const response = leaderboarddata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + {}, + response, + mockRes, + ); + }) + + test('Returns 200 if leaderboard data is empty and returns getUserLaborData', async () => { + const dashboardHelperObject = { + getLeaderboard: jest.fn(() => Promise.resolve([])), + getUserLaborData: jest.fn(() => Promise.resolve([])) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { leaderboarddata } = makeSut(); + + const response = leaderboarddata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + [], + response, + mockRes, + ); + }) + + test('Returns 400 if there is an error', async () => { + const dashboardHelperObject = { + getLeaderboard: jest.fn(() => Promise.reject({})) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { leaderboarddata } = makeSut(); + + const response = leaderboarddata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 400, + {}, + response, + mockRes, + ); + }) + }) + + describe('orgData Tests', () => { + + test('Returns 400 if there is an error in the function', async () => { + + const dashboardHelperObject = { + getOrgData: jest.fn(() => Promise.reject(error)) + }; + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { orgData } = makeSut(); + + const response = orgData(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 400, + error, + response, + mockRes, + ); + }) + + test('Returns 200 if the result is found and returns result', async () => { + const mockResult = { id: 1, name: 'Mock Results'}; + + const dashboardHelperObject = { + getOrgData: jest.fn(() => Promise.resolve([mockResult])) + } + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { orgData } = makeSut(); + + const response = orgData(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + mockResult, + response, + mockRes, + ); + }) + }); + + describe('dashboarddata Tests', () => { + test('Returns 200 if there is no error and return results', async () => { + + const dashboardHelperObject = { + personaldetails: jest.fn(() => Promise.resolve({})) + } + + dashboardHelperClosure.mockImplementationOnce(() => dashboardHelperObject); + + const { dashboarddata } = makeSut(); + + const response = dashboarddata(mockReq, mockRes); + + await flushPromises(); + + assertResMock( + 200, + {}, + response, + mockRes, + ) + }) + + }); + + describe('sendBugReport Tests', () => { + + test('Returns 200 if the bug report email is sent ', async () => { + + mockReq.body = { + ...mockReq.body, + firstName: 'Lin', + lastName: 'Test', + title: 'Bug in feature X', + environment: 'macOS 10.15, Chrome 89, App version 1.2.3', + reproduction: '1. Click on button A\n2. Enter valid data\n3. Click submit', + expected: 'The app should not display an error message', + actual: 'The app', + visual: 'Screenshot attached', + severity: 'High', + email: 'lin.test@example.com', + }; + + const { sendBugReport } = makeSut(); + + sendBugReport(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith('Success'); + }) + + test('Returns 500 if the email fails to send', async () => { + + mockReq.body = { + ...mockReq.body, + firstName: 'Lin', + lastName: 'Test', + title: 'Bug in feature X', + environment: 'macOS 10.15, Chrome 89, App version 1.2.3', + reproduction: '1. Click on button A\n2. Enter valid data\n3. Click submit', + expected: 'The app should not display an error message', + actual: 'The app', + visual: 'Screenshot attached', + severity: 'High', + email: 'lin.test@example.com', + }; + + emailSender.mockImplementation(() => { + throw new Error('Failed to send email'); + }); + + const { sendBugReport } = makeSut(); + + sendBugReport(mockReq, mockRes); + + emailSender.mockRejectedValue(new Error('Failed')); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith('Failed'); + }) + + }) + + describe('sendMakeSuggestion Tests', () => { + test('Returns 500 if the suggestion email fails to send', async () => { + + mockReq.body = { + suggestioncate: 'Identify and remedy poor client and/or user service experiences', + suggestion: 'This is a sample suggestion', + confirm: 'true', + email: 'test@example.com', + firstName: 'Lin', + lastName: 'Test', + field: ['field1', 'field2'], + }; + + emailSender.mockImplementation(() => { + throw new Error('Failed'); + }); + + const { sendMakeSuggestion } = makeSut(); + + sendMakeSuggestion(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith('Failed'); + }) + + test('Returns 200 if the suggestion email is sent successfully', async () => { + + mockReq.body = { + ...mockReq.body, + suggestioncate: 'Identify and remedy poor client and/or user service experiences', + suggestion: 'This is a sample suggestion', + confirm: 'true', + email: 'john.doe@example.com', + firstName: 'John', + lastName: 'Doe', + field: ['field1', 'field2'], + }; + + emailSender.mockImplementation(() => { + Promise.resolve(); + }); + + const { sendMakeSuggestion } = makeSut(); + + sendMakeSuggestion(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith('Success'); + }) + + }) + + // Need to make test cases for negative case + describe('getSuggestionOption Tests', () => { + + // test.only('Returns 404 if the suggestion data is not found', async () => { + + // const { getSuggestionOption } = makeSut(); + + // await getSuggestionOption(mockReq, mockRes); + + // await flushPromises(); + + // expect(mockRes.status).toHaveBeenCalledWith(404); + // expect(mockRes.send).toHaveBeenCalledWith('Suggestion Data Not Found'); + // }); + + test('Returns 200 if there is suggestion data', async () => { + + const suggestionData = { + "field": [], + "suggestion": [ + "Identify and remedy poor client and/or user service experiences", + "Identify bright spots and enhance positive service experiences", + "Make fundamental changes to our programs and/or operations", + "Inform the development of new programs/projects", + "Identify where we are less inclusive or equitable across demographic groups", + "Strengthen relationships with the people we serve", + "Understand people's needs and how we can help them achieve their goals", + "Other" + ] + }; + + const { getSuggestionOption } = makeSut(); + + await getSuggestionOption(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(suggestionData); + }) + }) + + // Need to make test cases for negative case + describe('editSuggestionOption tests', () => { + test('Returns 200 if suggestionData.field is added a new field', async () => { + + const suggestionData = { + suggestion: ['newSuggestion'], + field: ['newField'], + }; + + mockReq.body = { + suggestion: true, + action: 'add', + newField: 'new field', + }; + + const { editSuggestionOption } = makeSut(); + + await editSuggestionOption(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(suggestionData.field).toEqual(['newField']); + expect(mockRes.send).toHaveBeenCalledWith('success'); + }); + + test('Returns 200 if suggestionData.suggestion is added a new suggestion', async () => { + + const suggestionData = { + suggestion: ['newSuggestion'], + field: [], + }; + + mockReq.body = { + suggestion: true, + action: 'add', + newField: 'new suggestion', + }; + + const { editSuggestionOption } = makeSut(); + + await editSuggestionOption(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(suggestionData.suggestion).toEqual(['newSuggestion']); + expect(mockRes.send).toHaveBeenCalledWith('success'); + }) + + test('Returns 200 if suggestionData.field is deleted', async () => { + + const suggestionData = { + suggestion: ['newSuggestion'], + field: [], + }; + + mockReq.body = { + suggestion: true, + action: 'delete', + newField: 'new field', + }; + + const { editSuggestionOption } = makeSut(); + + await editSuggestionOption(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(suggestionData.field).toEqual([]); + expect(mockRes.send).toHaveBeenCalledWith('success'); + }); + + test('Returns 200 if suggestionData.suggestion is deleted', async () => { + + const suggestionData = { + suggestion: [], + field: [], + }; + + mockReq.body = { + suggestion: true, + action: 'delete', + newField: 'new field', + }; + + const { editSuggestionOption } = makeSut(); + + await editSuggestionOption(mockReq, mockRes); + + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(suggestionData.suggestion).toEqual([]); + expect(mockRes.send).toHaveBeenCalledWith('success'); + }); + + // test.only('Returns 500 if there is an error in the function', async () => { + + // const { editSuggestionOption } = makeSut(); + + // await editSuggestionOption(mockReq, mockRes); + + // jest + // .spyOn(console, 'error') + // .mockRejectedValueOnce('Internal Server Error') + + // expect(mockRes.status).toHaveBeenCalledWith(500); + // expect(mockRes.send).toHaveBeenCalledWith('Internal Server Error'); + // }); + + + }) + +}); diff --git a/src/controllers/forcePwdController.spec.js b/src/controllers/forcePwdController.spec.js new file mode 100644 index 000000000..a6d02d381 --- /dev/null +++ b/src/controllers/forcePwdController.spec.js @@ -0,0 +1,68 @@ +const forcePwdcontroller = require('./forcePwdController'); +const userProfile = require('../models/userProfile'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const makeSut = () => { + const { forcePwd } = forcePwdcontroller(userProfile); + + return { + forcePwd, + }; +}; + +const flushPromises = () => new Promise(setImmediate); + +describe('ForcePwdController Unit Tests', () => { + beforeEach(() => { + mockReq.body.userId = '65cf6c3706d8ac105827bb2e'; + mockReq.body.newpassword = 'newPasswordReset'; + + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns a 400 bad request status if userId is not valid with an error message Bad Request', async () => { + const { forcePwd } = makeSut(); + const errorMsg = { error: 'Bad Request' }; + mockReq.body.userId = ''; + const response = await forcePwd(mockReq, mockRes); + assertResMock(400, errorMsg, response, mockRes); + }); + test('Returns a 500 Internal Error if finding userProfile throws an error', async () => { + const { forcePwd } = makeSut(); + const errorMsg = 'Error happened when finding user'; + jest.spyOn(userProfile, 'findById').mockImplementationOnce(() => Promise.reject(errorMsg)); + const response = forcePwd(mockReq, mockRes); + await flushPromises(); + assertResMock(500, errorMsg, response, mockRes); + }); + test('Returns a 200 OK status with a success message "password Reset"', async () => { + const { forcePwd } = makeSut(); + const successMsg = { message: ' password Reset' }; + const mockUser = { + set: jest.fn(), + save: jest.fn().mockResolvedValue({}), + }; + + jest.spyOn(userProfile, 'findById').mockResolvedValue(mockUser); + + const response = forcePwd(mockReq, mockRes); + await flushPromises(); + assertResMock(200, successMsg, response, mockRes); + }); + test('Returns a 500 Internal Error status if new password fails to save', async () => { + const { forcePwd } = makeSut(); + const errorMsg = 'Error happened when saving user'; + const mockUser = { + set: jest.fn(), + save: jest.fn().mockRejectedValue(errorMsg), + }; + + jest.spyOn(userProfile, 'findById').mockResolvedValue(mockUser); + + const response = forcePwd(mockReq, mockRes); + await flushPromises(); + assertResMock(500, errorMsg, response, mockRes); + }); +}); diff --git a/src/controllers/forgotPwdcontroller.spec.js b/src/controllers/forgotPwdcontroller.spec.js new file mode 100644 index 000000000..f8e1f2a6b --- /dev/null +++ b/src/controllers/forgotPwdcontroller.spec.js @@ -0,0 +1,183 @@ +jest.mock('uuid/v4'); +jest.mock('../utilities/emailSender'); + +const uuidv4 = require('uuid/v4'); +const emailSender = require('../utilities/emailSender'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const forgotPwdController = require('./forgotPwdcontroller'); +const UserProfile = require('../models/userProfile'); +const escapeRegex = require('../utilities/escapeRegex'); + +uuidv4.mockReturnValue(''); +emailSender.mockImplementation(() => undefined); + +const flushPromises = () => new Promise(setImmediate); + +// Positive +// ✅ Return 200 if successfully generated temporary User password. + +// Negative +// ✅ Return 500 if any error encountered while fetching User details. +// ✅ Return 500 if any error encountered while saving User's password. +// ✅ Return 400 if valid user not found. + +function getEmailMessageForForgotPassword(user, ranPwd) { + const message = ` Hello ${user.firstName} ${user.lastName}, +

    Congratulations on successfully completing the Highest Good Network 3-question Change My Password Challenge. Your reward is this NEW PASSWORD!

    +
    ${ranPwd}
    +

    Use it now to log in. Then store it in a safe place or change it on your Profile Page to something easier for you to remember.

    +

    If it wasn’t you that requested this password change, you can ignore this email. Otherwise, use the password above to log in and you’ll be directed to the “Change Password” page where you can set a new custom one.

    +

    Thank you,

    +

    One Community

    `; + return message; +} + +const makeSut = () => { + const { forgotPwd } = forgotPwdController(UserProfile); + return { forgotPwd }; +}; + +describe('Unit Tests for forgotPwdcontroller.js', () => { + beforeAll(() => { + mockReq.body.email = 'parthgrads@gmail.com'; + mockReq.body.firstName = 'Parth'; + mockReq.body.lastName = 'Jangid'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Forgot Pwd Function', () => { + test('Returns 500 if any error encountered while fetching user.', async () => { + const { forgotPwd } = makeSut(); + + const error = new Error('Database error'); + const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockRejectedValueOnce(error); + + const response = await forgotPwd(mockReq, mockRes); + + assertResMock(500, error, response, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ + // Check Parameters to findOne + email: { + $regex: escapeRegex(mockReq.body.email), + $options: 'i', + }, + firstName: { + $regex: escapeRegex(mockReq.body.firstName), + $options: 'i', + }, + lastName: { + $regex: escapeRegex(mockReq.body.lastName), + $options: 'i', + }, + }); + }); + + test('Returns 400 if No Valid User found', async () => { + const { forgotPwd } = makeSut(); + + const userObject = null; // or undefined + const error = { error: 'No Valid user was found' }; + + const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(userObject); + + const response = await forgotPwd(mockReq, mockRes); + + assertResMock(400, error, response, mockRes); + + expect(findOneSpy).toHaveBeenCalledWith({ + email: { + $regex: escapeRegex(mockReq.body.email), + $options: 'i', + }, + firstName: { + $regex: escapeRegex(mockReq.body.firstName), + $options: 'i', + }, + lastName: { + $regex: escapeRegex(mockReq.body.lastName), + $options: 'i', + }, + }); + }); + + test('Return 500 if encountered any error while saving temporary password', async () => { + const { forgotPwd } = makeSut(); + + const error = new Error('Error Saving User Details'); + + const mockUser = { + set: jest.fn(), // Mocking the set method + save: jest.fn().mockRejectedValueOnce(error), // Mocked below using spyOn + }; + + const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(mockUser); + + const response = await forgotPwd(mockReq, mockRes); + await flushPromises(); + expect(mockUser.set).toHaveBeenCalled(); + expect(mockUser.save).toHaveBeenCalled(); + assertResMock(500, error, response, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ + email: { + $regex: escapeRegex(mockReq.body.email), + $options: 'i', + }, + firstName: { + $regex: escapeRegex(mockReq.body.firstName), + $options: 'i', + }, + lastName: { + $regex: escapeRegex(mockReq.body.lastName), + $options: 'i', + }, + }); + }); + + test('Return 200 if a temporary password is generated for the user', async () => { + const { forgotPwd } = makeSut(); + + const mockUser = { + // denote the User object obtained by find operation on MongoDB + email: mockReq.body.email, + firstName: mockReq.body.firstName, + lastName: mockReq.body.lastName, + set: jest.fn(), // Mocking the set method + save: jest.fn().mockResolvedValueOnce(), // Mocking the save method + }; + + const message = { message: 'generated new password' }; + const findOneSpy = jest.spyOn(UserProfile, 'findOne').mockResolvedValueOnce(mockUser); + + const response = await forgotPwd(mockReq, mockRes); + const temporaryPassword = uuidv4().concat('TEMP'); // The source code appends "TEMP" so does this line + + expect(mockUser.set).toHaveBeenCalled(); + expect(mockUser.save).toHaveBeenCalled(); + expect(emailSender).toHaveBeenCalledWith( + mockUser.email, + 'Account Password change', + getEmailMessageForForgotPassword(mockUser, temporaryPassword), + null, + null, + ); + assertResMock(200, message, response, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ + email: { + $regex: escapeRegex(mockReq.body.email), + $options: 'i', + }, + firstName: { + $regex: escapeRegex(mockReq.body.firstName), + $options: 'i', + }, + lastName: { + $regex: escapeRegex(mockReq.body.lastName), + $options: 'i', + }, + }); + }); + }); +}); diff --git a/src/controllers/inventoryController.js b/src/controllers/inventoryController.js index 14082bbc8..d126cc7e4 100644 --- a/src/controllers/inventoryController.js +++ b/src/controllers/inventoryController.js @@ -13,7 +13,7 @@ const inventoryController = function (Item, ItemType) { // use req.params.projectId and wbsId // Run a mongo query on the Item model to find all items with both the project and wbs // sort the mongo query so that the Wasted false items are listed first - await Item.find({ + return Item.find({ project: mongoose.Types.ObjectId(req.params.projectId), wbs: req.params.wbsId && req.params.wbsId !== 'Unassigned' @@ -283,9 +283,9 @@ const inventoryController = function (Item, ItemType) { } // update the original item by decreasing by the quantity and adding a note - if (req.body.quantity && req.param.invId && projectExists && wbsExists) { + if (req.body.quantity && req.params.invId && projectExists && wbsExists) { return Item.findByIdAndUpdate( - req.param.invId, + req.params.invId, { $decr: { quantity: req.body.quantity }, $push: { diff --git a/src/controllers/inventoryController.spec.js b/src/controllers/inventoryController.spec.js deleted file mode 100644 index 43f7f3682..000000000 --- a/src/controllers/inventoryController.spec.js +++ /dev/null @@ -1,334 +0,0 @@ -/* eslint-disable new-cap */ - -jest.mock('../utilities/permissions', () => ({ - hasPermission: jest.fn(), // Mocking the hasPermission function -})); -const { mockReq, mockRes, assertResMock } = require('../test'); - -const inventoryItem = require('../models/inventoryItem'); -const inventoryItemType = require('../models/inventoryItemType'); -const inventoryController = require('./inventoryController'); -const projects = require('../models/project'); -const wbs = require('../models/wbs'); - -const { hasPermission } = require('../utilities/permissions'); - -const makeSut = () => { - const { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject } = inventoryController( - inventoryItem, - inventoryItemType, - ); - return { getAllInvInProjectWBS, postInvInProjectWBS, getAllInvInProject }; -}; - -const flushPromises = () => new Promise(setImmediate); - -describe('Unit test for inventoryController', () => { - beforeAll(() => { - jest.clearAllMocks(); - }); - beforeEach(() => { - mockReq.params.userid = '5a7e21f00317bc1538def4b7'; - mockReq.params.userId = '5a7e21f00317bc1538def4b7'; - mockReq.params.wbsId = '5a7e21f00317bc1538def4b7'; - mockReq.params.projectId = '5a7e21f00317bc1538def4b7'; - mockReq.body = { - project: '5a7e21f00317bc1538def4b7', - wbs: '5a7e21f00317bc1538def4b7', - itemType: '5a7e21f00317bc1538def4b7', - item: '5a7e21f00317bc1538def4b7', - quantity: 1, - typeId: '5a7e21f00317bc1538def4b7', - cost: 20, - poNum: '123', - }; - }); - afterEach(() => { - jest.clearAllMocks(); - }); - describe('getAllInvInProjectWBS', () => { - test('Returns 403 if user is not authorized to view inventory data', async () => { - const { getAllInvInProjectWBS } = makeSut(); - hasPermission.mockResolvedValue(false); - const response = await getAllInvInProjectWBS(mockReq, mockRes); - assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test('Returns 404 if an error occurs while fetching inventory data', async () => { - const { getAllInvInProjectWBS } = makeSut(); - // Mocking hasPermission function - hasPermission.mockResolvedValue(true); - - // Mock error - const error = new Error('Error fetching inventory data'); - - // Mock chainable methods: populate, sort, then, catch - const mockInventoryItem = { - populate: jest.fn().mockReturnThis(), - sort: jest.fn().mockReturnThis(), - then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), - catch: jest.fn().mockReturnThis(), - }; - - // Mock the inventoryItem.find method - jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); - - // Call the function - const response = await getAllInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - - // Assertions - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(404, error, response, mockRes); - }); - - test('Returns 200 if successfully found data', async () => { - const { getAllInvInProjectWBS } = makeSut(); - hasPermission.mockResolvedValue(true); - - const mockData = [ - { - _id: '123', - project: '123', - wbs: '123', - itemType: '123', - item: '123', - quantity: 1, - date: new Date().toISOString(), - }, - ]; - - const mockInventoryItem = { - populate: jest.fn().mockReturnThis(), - sort: jest.fn().mockResolvedValue(mockData), - then: jest.fn().mockResolvedValue(() => {}), - catch: jest.fn().mockReturnThis(), - }; - - // Mock the inventoryItem.find method - jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); - - // Call the function - const response = await getAllInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - - // Assertions - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(200, mockData, response, mockRes); - }); - }); - describe('postInvInProjectWBS', () => { - test('Returns error 403 if the user is not authorized to view data', async () => { - const { getAllInvInProjectWBS } = makeSut(); - hasPermission.mockReturnValue(false); - const response = await getAllInvInProjectWBS(mockReq, mockRes); - assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test('Returns error 400 if an error occurs while fetching an item', async () => { - mockReq.params.wbsId = 'Unassigned'; - const { postInvInProjectWBS } = makeSut(); - hasPermission.mockReturnValue(true); - // look up difference betewewen mockimplmenonce and mockimplementation - // how to incorpoate into the test - // and how to setup mocking variables as well - const mockProjectExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnValue(null), - }; - - jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); - - const response = await postInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock( - 400, - 'Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned', - response, - mockRes, - ); - }); - test('Returns error 500 if an error occurs when saving', async () => { - const mockProjectExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockWbsExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockInventoryItem = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnValue(null), - }; - const { postInvInProjectWBS } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - hasPermission.mockReturnValue(true); - - jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); - jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); - jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); - - jest.spyOn(inventoryItem.prototype, 'save').mockRejectedValueOnce(new Error('Error saving')); - const response = await postInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(500, new Error('Error saving'), response, mockRes); - }); - - test('Receives a 201 success if the inventory was successfully created and saved', async () => { - const resolvedInventoryItem = new inventoryItem({ - project: mockReq.body.projectId, - wbs: mockReq.body.wbsId, - type: mockReq.body.typeId, - quantity: mockReq.body.quantity, - cost: mockReq.body.cost, - poNum: mockReq.body.poNum, - }); - const mockProjectExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockWbsExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockInventoryItem = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnValue(null), - }; - const { postInvInProjectWBS } = makeSut(); - - hasPermission.mockReturnValue(true); - jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); - jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); - jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryItem); - jest - .spyOn(inventoryItem.prototype, 'save') - .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); - - const response = await postInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(201, resolvedInventoryItem, response, mockRes); - }); - - test('Returns a 201, if the inventory item was succesfully updated and saved.', async () => { - const resolvedInventoryItem = { - project: mockReq.body.projectId, - wbs: mockReq.body.wbsId, - type: mockReq.body.typeId, - quantity: mockReq.body.quantity, - cost: mockReq.body.cost, - poNum: mockReq.body.poNum, - }; - - const updatedResolvedInventoryItem = { - project: mockReq.body.projectId, - wbs: mockReq.body.wbsId, - type: mockReq.body.typeId, - quantity: mockReq.body.quantity + 1, - costPer: 200, - }; - - const mockProjectExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockWbsExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - const mockInventoryExists = { - select: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - }; - - const { postInvInProjectWBS } = makeSut(); - hasPermission.mockReturnValue(true); - - jest.spyOn(projects, 'findOne').mockImplementationOnce(() => mockProjectExists); - jest.spyOn(wbs, 'findOne').mockImplementationOnce(() => mockWbsExists); - jest.spyOn(inventoryItem, 'findOne').mockImplementationOnce(() => mockInventoryExists); - jest - .spyOn(inventoryItem, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve(resolvedInventoryItem)); - - jest - .spyOn(inventoryItem, 'findByIdAndUpdate') - .mockImplementationOnce(() => Promise.resolve(updatedResolvedInventoryItem)); - - const response = await postInvInProjectWBS(mockReq, mockRes); - await flushPromises(); - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(201, updatedResolvedInventoryItem, response, mockRes); - }); - }); - - describe('getAllInvInProject', () => { - test('Returns 403 if user is not authorized to view inventory data', async () => { - const { getAllInvInProject } = makeSut(); - hasPermission.mockResolvedValue(false); - const response = await getAllInvInProject(mockReq, mockRes); - assertResMock(403, 'You are not authorized to view inventory data.', response, mockRes); - expect(hasPermission).toHaveBeenCalledTimes(1); - }); - - test('Returns 404 if an error occurs while fetching inventory data', async () => { - const { getAllInvInProject } = makeSut(); - hasPermission.mockResolvedValue(true); - - const error = new Error('Error fetching inventory data'); - - const mockInventoryItem = { - populate: jest.fn().mockReturnThis(), - sort: jest.fn().mockReturnThis(), - then: jest.fn().mockImplementationOnce(() => Promise.reject(error)), - catch: jest.fn().mockReturnThis(), - }; - - jest.spyOn(inventoryItem, 'find').mockImplementationOnce(() => mockInventoryItem); - - const response = await getAllInvInProject(mockReq, mockRes); - await flushPromises(); - - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(404, error, response, mockRes); - }); - - test('Returns 200 if successfully found data', async () => { - const { getAllInvInProject } = makeSut(); - hasPermission.mockResolvedValue(true); - - const mockData = [ - { - _id: '123', - project: '123', - wbs: '123', - itemType: '123', - item: '123', - quantity: 1, - date: new Date().toISOString(), - }, - ]; - - const mockInventoryItem = { - populate: jest.fn().mockReturnThis(), - sort: jest.fn().mockResolvedValue(mockData), - catch: jest.fn().mockReturnThis(), - }; - - jest.spyOn(inventoryItem, 'find').mockImplementation(() => mockInventoryItem); - - const response = await getAllInvInProject(mockReq, mockRes); - await flushPromises(); - - expect(hasPermission).toHaveBeenCalledTimes(1); - assertResMock(200, mockData, response, mockRes); - }); - }); -}); diff --git a/src/controllers/logincontroller.js b/src/controllers/logincontroller.js index 809c5892f..3ba0203aa 100644 --- a/src/controllers/logincontroller.js +++ b/src/controllers/logincontroller.js @@ -23,7 +23,10 @@ const logincontroller = function () { if (!user) { res.status(403).send({ message: 'Username not found.' }); } else if (user.isActive === false) { - res.status(403).send({ message: 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.' }); + res.status(403).send({ + message: + 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.', + }); } else { let isPasswordMatch = false; let isNewUser = false; @@ -34,42 +37,42 @@ const logincontroller = function () { isPasswordMatch = await bcrypt.compare(_password, user.password); if (!isPasswordMatch && user.resetPwd !== '') { - isPasswordMatch = (_password === user.resetPwd); + isPasswordMatch = _password === user.resetPwd; isNewUser = true; } - if (isNewUser && isPasswordMatch) { - const result = { - new: true, - userId: user._id, - }; - res.status(200).send(result); - } else if (isPasswordMatch && !isNewUser) { - const jwtPayload = { - userid: user._id, - role: user.role, - permissions: user.permissions, - access: { - canAccessBMPortal: false, - }, - email: user.email, - expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), - }; + if (isNewUser && isPasswordMatch) { + const result = { + new: true, + userId: user._id, + }; + res.status(200).send(result); + } else if (isPasswordMatch && !isNewUser) { + const jwtPayload = { + userid: user._id, + role: user.role, + permissions: user.permissions, + access: { + canAccessBMPortal: false, + }, + email: user.email, + expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), + }; - const token = jwt.sign(jwtPayload, JWT_SECRET); + const token = jwt.sign(jwtPayload, JWT_SECRET); - res.status(200).send({ token }); - } else { - res.status(403).send({ - message: 'Invalid password.', - }); - } + res.status(200).send({ token }); + } else { + res.status(403).send({ + message: 'Invalid password.', + }); + } } - } catch (err) { - console.log(err); - res.json(err); - } -}; + } catch (err) { + console.log(err); + res.json(err); + } + }; const getUser = function (req, res) { const { requestor } = req.body; @@ -78,7 +81,6 @@ const logincontroller = function () { }; return { - login, getUser, }; diff --git a/src/controllers/logincontroller.spec.js b/src/controllers/logincontroller.spec.js new file mode 100644 index 000000000..595bfe77b --- /dev/null +++ b/src/controllers/logincontroller.spec.js @@ -0,0 +1,211 @@ +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); +const bcrypt = require('bcryptjs'); +const logincontroller = require('./logincontroller'); +const { mockReq, mockRes, assertResMock, mockUser } = require('../test'); +const userProfile = require('../models/userProfile'); + +const makeSut = () => { + const { login, getUser } = logincontroller(); + return { + login, + getUser, + }; +}; + +describe('logincontroller module', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('login', () => { + test('Ensure login returns error 400 if there is no email or password', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: '', + password: '', + }, + }, + }; + const res = await login(mockReqModified, mockRes); + assertResMock(400, { error: 'Invalid request' }, res, mockRes); + }); + + test('Ensure login returns error 403 if there is no user', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: 'example@test.com', + password: 'exampletest', + }, + }, + }; + const findOneSpy = jest + .spyOn(userProfile, 'findOne') + .mockImplementation(() => Promise.resolve(null)); + + const res = await login(mockReqModified, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); + assertResMock(403, { message: 'Username not found.' }, res, mockRes); + }); + + test('Ensure login returns error 403 if the user exists but is not active', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: 'example@test.com', + password: 'exampletest', + }, + }, + }; + const mockUserModified = { + ...mockUser, + ...{ + isActive: false, + }, + }; + + const findOneSpy = jest + .spyOn(userProfile, 'findOne') + .mockImplementation(() => Promise.resolve(mockUserModified)); + + const res = await login(mockReqModified, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); + assertResMock( + 403, + { + message: + 'Sorry, this account is no longer active. If you feel this is in error, please contact your Manager and/or Administrator.', + }, + res, + mockRes, + ); + }); + + test('Ensure login returns error 403 if the password is not a match and if the user already exists', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: 'example@test.com', + password: 'SuperSecretPassword@', + }, + }, + }; + + const findOneSpy = jest + .spyOn(userProfile, 'findOne') + .mockImplementation(() => Promise.resolve(mockUser)); + jest.spyOn(bcrypt, 'compare').mockResolvedValue(false); + + const res = await login(mockReqModified, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); + + assertResMock( + 403, + { + message: 'Invalid password.', + }, + res, + mockRes, + ); + }); + + test('Ensure login returns the error if the try block fails', async () => { + const { login } = makeSut(); + const error = new Error('Try block failed'); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: 'example@test.com', + password: 'exampletest', + }, + }, + }; + + jest.spyOn(userProfile, 'findOne').mockImplementation(() => Promise.reject(error)); + + await login(mockReqModified, mockRes); + expect(mockRes.json).toHaveBeenCalledWith(error); + }); + + test('Ensure login returns 200, if the user is a new user and there is a password match', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + ...{ + body: { + email: 'example@example.com', + password: '123Welcome!', + }, + }, + }; + + const mockUserModified = { + _id: 'user123', + email: 'example@example.com', + password: 'hashedPassword', + resetPwd: 'newUserPassword', + isActive: true, + }; + + jest + .spyOn(userProfile, 'findOne') + .mockImplementation(() => Promise.resolve(mockUserModified)); + + jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); + + const res = await login(mockReqModified, mockRes); + assertResMock(200, { new: true, userId: 'user123' }, res, mockRes); + }); + + test('Ensure login returns 200, if the user already exists and the password is a match', async () => { + const { login } = makeSut(); + const mockReqModified = { + ...mockReq, + body: { + email: 'existing@example.com', + password: 'existingUserPassword', + }, + }; + const mockUserModified = { + _id: 'user123', + email: 'existing@example.com', + password: 'hashedPassword', + resetPwd: 'newUserPassword', + isActive: true, + role: 'Volunteer', + permissions: ['read', 'write'], + }; + + const findOneSpy = jest + .spyOn(userProfile, 'findOne') + .mockImplementation(() => Promise.resolve(mockUserModified)); + + jest.spyOn(bcrypt, 'compare').mockResolvedValue(true); + + const res = await login(mockReqModified, mockRes); + expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); + + assertResMock(200, { token: expect.any(String) }, res, mockRes); + }); + }); + + describe('getUser', () => { + it('Ensure getUser returns 200, with the requestor body', () => { + const { getUser } = makeSut(); + + const res = getUser(mockReq, mockRes); + assertResMock(200, mockReq.body.requestor, res, mockRes); + }); + }); +}); diff --git a/src/controllers/mapLocationsController.js b/src/controllers/mapLocationsController.js index a2c98fcf1..bb85fe9f3 100644 --- a/src/controllers/mapLocationsController.js +++ b/src/controllers/mapLocationsController.js @@ -1,28 +1,24 @@ const UserProfile = require('../models/userProfile'); -const cacheClosure = require('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache')(); const mapLocationsController = function (MapLocation) { - const cache = cacheClosure(); const getAllLocations = async function (req, res) { try { const users = []; const results = await UserProfile.find( - {}, +{}, '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', - ); +); results.forEach((item) => { if ( - (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || - (item.location?.coords.lat && - item.location?.coords.lng && - // eslint-disable-next-line no-use-before-define - calculateTotalHours(item.hoursByCategory) >= 10) + (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) + || (item.location?.coords.lat && item.location?.coords.lng && calculateTotalHours(item.hoursByCategory) >= 10) ) { users.push(item); } }); - const modifiedUsers = users.map((item) => ({ + const modifiedUsers = users.map(item => ({ location: item.homeCountry || item.location, isActive: item.isActive, jobTitle: item.jobTitle[0], @@ -38,7 +34,7 @@ const mapLocationsController = function (MapLocation) { } }; const deleteLocation = async function (req, res) { - if (req.body.requestor.role !== 'Administrator' && req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Administrator' || !req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -46,14 +42,13 @@ const mapLocationsController = function (MapLocation) { MapLocation.findOneAndDelete({ _id: locationId }) .then(() => res.status(200).send({ message: 'The location was successfully removed!' })) - .catch((error) => res.status(500).send({ message: error || "Couldn't remove the location" })); + .catch(error => res.status(500).send({ message: error || "Couldn't remove the location" })); }; const putUserLocation = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } - const locationData = { firstName: req.body.firstName, lastName: req.body.lastName, @@ -70,11 +65,11 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(response); } catch (err) { console.log(err.message); - res.status(500).send({ message: err.message || 'Something went wrong...' }); + res.status(500).json({ message: err.message || 'Something went wrong...' }); } }; const updateUserLocation = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if (!req.body.requestor.role === 'Owner') { res.status(403).send('You are not authorized to make changes in the teams.'); return; } @@ -94,21 +89,13 @@ const mapLocationsController = function (MapLocation) { try { let response; if (userType === 'user') { - response = await UserProfile.findOneAndUpdate( - { _id: userId }, - { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, - { new: true }, - ); + response = await UserProfile.findOneAndUpdate({ _id: userId }, { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, { new: true }); cache.removeCache('allusers'); cache.removeCache(`user-${userId}`); cache.setCache(`user-${userId}`, JSON.stringify(response)); } else { - response = await MapLocation.findOneAndUpdate( - { _id: userId }, - { $set: updateData }, - { new: true }, - ); + response = await MapLocation.findOneAndUpdate({ _id: userId }, { $set: updateData }, { new: true }); } if (!response) { @@ -126,7 +113,7 @@ const mapLocationsController = function (MapLocation) { res.status(200).send(newData); } catch (err) { console.log(err.message); - res.status(500).send({ message: err.message || 'Something went wrong...' }); + res.status(500).json({ message: err.message || 'Something went wrong...' }); } }; diff --git a/src/controllers/mapLocationsController.spec.js b/src/controllers/mapLocationsController.spec.js deleted file mode 100644 index 871ca1088..000000000 --- a/src/controllers/mapLocationsController.spec.js +++ /dev/null @@ -1,367 +0,0 @@ -/// mock the cache function before importing so we can manipulate the implementation - -jest.mock('../utilities/nodeCache'); -const cache = require('../utilities/nodeCache'); -const MapLocation = require('../models/mapLocation'); -const UserProfile = require('../models/userProfile'); -const { mockReq, mockRes, assertResMock } = require('../test'); -const mapLocationsController = require('./mapLocationsController'); - -const makeSut = () => { - const { getAllLocations, deleteLocation, putUserLocation, updateUserLocation } = - mapLocationsController(MapLocation); - - return { getAllLocations, deleteLocation, putUserLocation, updateUserLocation }; -}; - -const flushPromises = () => new Promise(setImmediate); - -const makeMockCache = (method, value) => { - const cacheObject = { - getCache: jest.fn(), - removeCache: jest.fn(), - hasCache: jest.fn(), - setCache: jest.fn(), - }; - - const mockCache = jest.spyOn(cacheObject, method).mockImplementationOnce(() => value); - - cache.mockImplementationOnce(() => cacheObject); - - return { mockCache, cacheObject }; -}; - -describe('Map Locations Controller', () => { - beforeEach(() => { - mockReq.params.locationId = 'randomId'; - mockReq.body.firstName = 'Bob'; - mockReq.body.lastName = 'Bobberson'; - mockReq.body.jobTitle = 'Software Engineer'; - mockReq.body.location = { - userProvided: 'New York', - coords: { - lat: 12, - lng: 12, - }, - country: 'USA', - city: 'New York City', - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('getAllLocations method', () => { - test('Returns 404 if an error occurs when finding all users.', async () => { - const { getAllLocations } = makeSut(); - - const errMsg = 'Failed to find users!'; - const findSpy = jest.spyOn(UserProfile, 'find').mockRejectedValueOnce(new Error(errMsg)); - - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(404, new Error(errMsg), res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', - ); - }); - - test('Returns 404 if an error occurs when finding all map locations.', async () => { - const { getAllLocations } = makeSut(); - - const errMsg = 'Failed to find locations!'; - const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce([]); - const findLocationSpy = jest - .spyOn(MapLocation, 'find') - .mockRejectedValueOnce(new Error(errMsg)); - - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(404, new Error(errMsg), res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', - ); - expect(findLocationSpy).toHaveBeenCalledWith({}); - }); - - test('Returns 200 if all is successful', async () => { - const { getAllLocations } = makeSut(); - - const findRes = [ - { - _id: 1, - firstName: 'bob', - lastName: 'marley', - isActive: true, - location: { - coords: { - lat: 12, - lng: 12, - }, - country: 'USA', - city: 'NYC', - }, - jobTitle: ['software engineer'], - totalTangibleHrs: 11, - }, - ]; - const findSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce(findRes); - const findLocationSpy = jest.spyOn(MapLocation, 'find').mockResolvedValueOnce([]); - - const modifiedUsers = { - location: findRes[0].location, - isActive: findRes[0].isActive, - jobTitle: findRes[0].jobTitle[0], - _id: findRes[0]._id, - firstName: findRes[0].firstName, - lastName: findRes[0].lastName, - }; - const res = await getAllLocations(mockReq, mockRes); - - assertResMock(200, { users: [modifiedUsers], mUsers: [] }, res, mockRes); - expect(findSpy).toHaveBeenCalledWith( - {}, - '_id firstName lastName isActive location jobTitle totalTangibleHrs hoursByCategory homeCountry', - ); - expect(findLocationSpy).toHaveBeenCalledWith({}); - }); - }); - - describe('deleteLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - mockReq.body.requestor.role = 'Volunteer'; - const { deleteLocation } = makeSut(); - const res = await deleteLocation(mockReq, mockRes); - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - test('Returns 500 if an error occurs when deleting the map location.', async () => { - mockReq.body.requestor.role = 'Owner'; - - const { deleteLocation } = makeSut(); - - const err = new Error('Failed to delete!'); - const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockRejectedValueOnce(err); - - const res = await deleteLocation(mockReq, mockRes); - await flushPromises(); - - assertResMock(500, { message: err }, res, mockRes); - expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); - }); - - test('Returns 200 if all is successful', async () => { - mockReq.body.requestor.role = 'Owner'; - const { deleteLocation } = makeSut(); - - const deleteSpy = jest.spyOn(MapLocation, 'findOneAndDelete').mockResolvedValueOnce(true); - - const res = await deleteLocation(mockReq, mockRes); - await flushPromises(); - - assertResMock(200, { message: 'The location was successfully removed!' }, res, mockRes); - expect(deleteSpy).toHaveBeenCalledWith({ _id: mockReq.params.locationId }); - }); - }); - - describe('putUserLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - mockReq.body.requestor.role = 'Volunteer'; - const { putUserLocation } = makeSut(); - - const res = await putUserLocation(mockReq, mockRes); - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - test('Returns 500 if an error occurs when saving the map location.', async () => { - const { putUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Owner'; - - const err = new Error('Saving failed!'); - - jest.spyOn(MapLocation.prototype, 'save').mockImplementationOnce(() => Promise.reject(err)); - - const res = await putUserLocation(mockReq, mockRes); - - assertResMock(500, { message: err.message }, res, mockRes); - }); - - test('Returns 200 if all is successful.', async () => { - const { putUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Owner'; - - const savedLocationData = { - _id: 1, - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - jest - .spyOn(MapLocation.prototype, 'save') - .mockImplementationOnce(() => Promise.resolve(savedLocationData)); - - const res = await putUserLocation(mockReq, mockRes); - - assertResMock(200, savedLocationData, res, mockRes); - }); - }); - - describe('updateUserLocation method', () => { - test('Returns 403 if user is not authorized.', async () => { - const { updateUserLocation } = makeSut(); - - mockReq.body.requestor.role = 'Volunteer'; - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(403, 'You are not authorized to make changes in the teams.', res, mockRes); - }); - - // Returns 500 if an error occurs when updating the user location. - test('Returns 500 if an error occurs when updating the user location', async () => { - const { updateUserLocation } = makeSut(); - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const errMsg = 'Failed to update user profile!'; - const findAndUpdateSpy = jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); - expect(findAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, - { new: true }, - ); - }); - - test('returns 500 if an error occurs when updating map location', async () => { - const { updateUserLocation } = makeSut(); - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'non-user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const errMsg = 'failed to update map locations!'; - const findAndUpdateSpy = jest - .spyOn(MapLocation, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.reject(new Error(errMsg))); - - const res = await updateUserLocation(mockReq, mockRes); - assertResMock(500, { message: new Error(errMsg).message }, res, mockRes); - expect(findAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: updateData }, - { new: true }, - ); - }); - - test('Returns 200 if all is successful when userType is user and clears and resets cache.', async () => { - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - - const { mockCache: removeAllUsersMock, cacheObject } = makeMockCache('removeCache', true); - const removeUserCacheSpy = jest - .spyOn(cacheObject, 'removeCache') - .mockImplementationOnce(() => true); - - const setCacheSpy = jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => true); - - const { updateUserLocation } = makeSut(); - - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const queryResponse = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - _id: mockReq.body._id, - }; - - const findOneAndUpdateSpy = jest - .spyOn(UserProfile, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve(queryResponse)); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); - expect(findOneAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: { ...updateData, jobTitle: [updateData.jobTitle] } }, - { new: true }, - ); - - expect(removeAllUsersMock).toHaveBeenCalledWith('allusers'); - expect(removeUserCacheSpy).toHaveBeenCalledWith(`user-${mockReq.body._id}`); - expect(setCacheSpy).toHaveBeenCalledWith( - `user-${mockReq.body._id}`, - JSON.stringify(queryResponse), - ); - }); - - test('Returns 200 if all is succesful when userType is not user', async () => { - mockReq.body.requestor.role = 'Owner'; - mockReq.body.type = 'not-user'; - mockReq.body._id = '60d5f60c2f9b9c3b8a1e4a2f'; - - const { updateUserLocation } = makeSut(); - - const updateData = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - }; - - const queryResponse = { - firstName: mockReq.body.firstName, - lastName: mockReq.body.lastName, - jobTitle: mockReq.body.jobTitle, - location: mockReq.body.location, - _id: mockReq.body._id, - }; - - const findOneAndUpdateSpy = jest - .spyOn(MapLocation, 'findOneAndUpdate') - .mockImplementationOnce(() => Promise.resolve(queryResponse)); - - const res = await updateUserLocation(mockReq, mockRes); - - assertResMock(200, { ...queryResponse, type: mockReq.body.type }, res, mockRes); - expect(findOneAndUpdateSpy).toHaveBeenCalledWith( - { _id: mockReq.body._id }, - { $set: updateData }, - { new: true }, - ); - }); - }); -}); diff --git a/src/controllers/mouseoverTextController.js b/src/controllers/mouseoverTextController.js index 74fae9847..636f8763a 100644 --- a/src/controllers/mouseoverTextController.js +++ b/src/controllers/mouseoverTextController.js @@ -1,43 +1,49 @@ -const mouseoverTextController = (function (MouseoverText) { - const createMouseoverText = function (req, res) { - const newMouseoverText = new MouseoverText(); - newMouseoverText.mouseoverText = req.body.newMouseoverText; - newMouseoverText.save().then(() => res.status(201).json({ - _serverMessage: 'MouseoverText succesfuly created!', - mouseoverText: newMouseoverText, - })).catch(err => res.status(500).send({ err })); - }; +const mouseoverTextController = function (MouseoverText) { + const createMouseoverText = function (req, res) { + const newMouseoverText = new MouseoverText(); + newMouseoverText.mouseoverText = req.body.newMouseoverText; + newMouseoverText + .save() + .then(() => + res.status(201).json({ + _serverMessage: 'MouseoverText succesfuly created!', + mouseoverText: newMouseoverText, + }), + ) + .catch((err) => res.status(500).send({ err })); + }; - const getMouseoverText = function (req, res) { - MouseoverText.find() - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send(error)); - }; + const getMouseoverText = function (req, res) { + MouseoverText.find() + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); + }; - const updateMouseoverText = function (req, res) { - // if (req.body.requestor.role !== 'Owner') { - // res.status(403).send('You are not authorized to update mouseoverText!'); - // } - const { id } = req.params; + const updateMouseoverText = function (req, res) { + // if (req.body.requestor.role !== 'Owner') { + // res.status(403).send('You are not authorized to update mouseoverText!'); + // } + const { id } = req.params; - return MouseoverText.findById(id, (error, mouseoverText) => { - if (error || mouseoverText === null) { - res.status(500).send('MouseoverText not found with the given ID'); - return; - } + return MouseoverText.findById(id, (error, mouseoverText) => { + if (error || mouseoverText === null) { + res.status(500).send('MouseoverText not found with the given ID'); + return; + } - mouseoverText.mouseoverText = req.body.newMouseoverText; - mouseoverText.save() - .then(results => res.status(201).send(results)) - .catch(errors => res.status(400).send(errors)); - }); - }; + mouseoverText.mouseoverText = req.body.newMouseoverText; + mouseoverText + .save() + .then((results) => res.status(201).send(results)) + .catch((errors) => res.status(400).send(errors)); + }); + }; - return { - createMouseoverText, - getMouseoverText, - updateMouseoverText, - }; -}); + return { + createMouseoverText, + getMouseoverText, + updateMouseoverText, + }; +}; module.exports = mouseoverTextController; diff --git a/src/controllers/mouseoverTextController.spec.js b/src/controllers/mouseoverTextController.spec.js new file mode 100644 index 000000000..b4a5bd48b --- /dev/null +++ b/src/controllers/mouseoverTextController.spec.js @@ -0,0 +1,162 @@ +const mouseoverTextController = require('./mouseoverTextController'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const MouseoverText = require('../models/mouseoverText'); + +const makeSut = () => { + const { createMouseoverText, getMouseoverText, updateMouseoverText } = + mouseoverTextController(MouseoverText); + return { createMouseoverText, getMouseoverText, updateMouseoverText }; +}; + +const flushPromises = () => new Promise(setImmediate); +describe('mouseoverText Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockReq.params.id = '6237f9af9820a0134ca79c5g'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createMouseoverText method', () => { + test('Ensure createMouseoverText returns 500 if any error when saving new mouseoverText', async () => { + const { createMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some mouseoverText', + }, + }; + jest + .spyOn(MouseoverText.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + + const response = createMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(500, { err: new Error('Error when saving') }, response, mockRes); + }); + test('Ensure createMouseoverText returns 201 if create new mouseoverText successfully', async () => { + const { createMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'new mouseoverText', + }, + }; + mockRes.json = jest.fn(); + jest.spyOn(MouseoverText.prototype, 'save').mockResolvedValue({ + mouseoverText: 'new mouseoverText', + }); + createMouseoverText(newMockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(201); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + mouseoverText: expect.objectContaining({ + mouseoverText: 'new mouseoverText', + }), + }), + ); + }); + }); + describe('getMouseoverText method', () => { + test('Ensure getMouseoverText returns 404 if any error when finding the mouseoverText', async () => { + const { getMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + mouseoverText: 'some mouseoverText', + }, + }; + jest + .spyOn(MouseoverText, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + + const response = getMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(404, new Error('Error when finding'), response, mockRes); + }); + test('Ensure getMouseoverText returns 200 if get the mouseoverText successfully', async () => { + const { getMouseoverText } = makeSut(); + const data = { + mouseoverText: 'some get mouseoverText', + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + mouseoverText: 'get mouseoverText', + }, + }; + jest.spyOn(MouseoverText, 'find').mockImplementationOnce(() => Promise.resolve(data)); + const response = getMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(200, data, response, mockRes); + }); + }); + describe('updateMouseoverText method', () => { + test('Ensure updateMouseoverText returns 500 if any error when finding the mouseoverText by Id', async () => { + const { updateMouseoverText } = makeSut(); + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(true, null)); + const response = updateMouseoverText(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, 'MouseoverText not found with the given ID', response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + }); + test('Ensure updateMouseoverText returns 400 if any error when saving the mouseoverText', async () => { + const { updateMouseoverText } = makeSut(); + const data = { + mouseoverText: 'old mouseoverText', + save: () => {}, + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some new mouseoverText', + }, + }; + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(false, data)); + jest.spyOn(data, 'save').mockRejectedValueOnce(new Error('Error when saving')); + const response = updateMouseoverText(newMockReq, mockRes); + await flushPromises(); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + assertResMock(400, new Error('Error when saving'), response, mockRes); + }); + test('Ensure updateMouseoverText returns 201 if updating mouseoverText successfully', async () => { + const { updateMouseoverText } = makeSut(); + const data = { + mouseoverText: 'some get mouseoverText', + save: () => {}, + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some new mouseoverText', + }, + }; + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(false, data)); + jest.spyOn(data, 'save').mockResolvedValueOnce(data); + const response = updateMouseoverText(newMockReq, mockRes); + await flushPromises(); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + assertResMock(201, data, response, mockRes); + }); + }); +}); diff --git a/src/controllers/notificationController.js b/src/controllers/notificationController.js index 98911c3f9..e15f0d989 100644 --- a/src/controllers/notificationController.js +++ b/src/controllers/notificationController.js @@ -3,8 +3,8 @@ const LOGGER = require('../startup/logger'); /** * API endpoint for notifications service. - * @param {} Notification - * @returns + * @param {} Notification + * @returns */ const notificationController = function () { @@ -18,15 +18,17 @@ const notificationController = function () { const getUserNotifications = async function (req, res) { const { userId } = req.params; const { requestor } = req.body; - if (requestor.requestorId !== userId && (requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { - res.status(403).send({ error: 'Unauthorized request' }); - return; - } - if (!userId) { res.status(400).send({ error: 'User ID is required' }); return; } + if ( + requestor.requestorId !== userId && + (requestor.role !== 'Administrator' || requestor.role !== 'Owner') + ) { + res.status(403).send({ error: 'Unauthorized request' }); + return; + } try { const result = await notificationService.getNotifications(userId); @@ -37,7 +39,7 @@ const notificationController = function () { } }; - /** + /** * This function allows the user to get unread notifications for themselves or * allows the admin/owner user to get unread notifications for a specific user. * @param {Object} req - The request with userID as request param. @@ -47,15 +49,17 @@ const notificationController = function () { const getUnreadUserNotifications = async function (req, res) { const { userId } = req.params; const { requestor } = req.body; - if (requestor.requestorId !== userId && (requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { - res.status(403).send({ error: 'Unauthorized request' }); - return; - } - if (!userId) { res.status(400).send({ error: 'User ID is required' }); return; } + if ( + requestor.requestorId !== userId && + (requestor.role !== 'Administrator' || requestor.role !== 'Owner') + ) { + res.status(403).send({ error: 'Unauthorized request' }); + return; + } try { const result = await notificationService.getUnreadUserNotifications(userId); @@ -68,13 +72,13 @@ const notificationController = function () { /** * This function allows the admin/owner user to get all notifications that they have sent. - * @param {*} req - * @param {*} res - * @returns + * @param {*} req + * @param {*} res + * @returns */ const getSentNotifications = async function (req, res) { const { requestor } = req.body; - if ((requestor.role !== 'Administrator' || requestor.role !== 'Owner')) { + if (requestor.role !== 'Administrator' && requestor.role !== 'Owner') { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -88,18 +92,17 @@ const notificationController = function () { } }; - /** * This function allows the Administrator/Owner user to create a notification to specific user. * @param {*} req request with a JSON payload containing the message and recipient list. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const createUserNotification = async function (req, res) { const { message, recipient } = req.body; const sender = req.requestor.requestorId; - if (req.body.requestor.role !== 'Administrator' || req.body.requestor.role !== 'Owner') { + if (req.body.requestor.role !== 'Administrator' && req.body.requestor.role !== 'Owner') { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -121,13 +124,13 @@ const notificationController = function () { /** * This function allows the Administrator/Owner user to delete a notification. * @param {*} req request with the notification ID as a parameter. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const deleteUserNotification = async function (req, res) { const { requestor } = req.body; - if (requestor.role !== 'Administrator' || requestor.role !== 'Owner') { + if (requestor.role !== 'Administrator' && requestor.role !== 'Owner') { res.status(403).send({ error: 'Unauthorized request' }); return; } @@ -144,8 +147,8 @@ const notificationController = function () { /** * This function allows the user to mark a notification as read. * @param {*} req request with the notification ID as a parameter. - * @param {*} res - * @returns + * @param {*} res + * @returns */ const markNotificationAsRead = async function (req, res) { const recipientId = req.body.requestor.requestorId; @@ -156,7 +159,10 @@ const notificationController = function () { } try { - const result = await notificationService.markNotificationAsRead(req.params.notificationId, recipientId); + const result = await notificationService.markNotificationAsRead( + req.params.notificationId, + recipientId, + ); res.status(200).send(result); } catch (err) { LOGGER.logException(err); diff --git a/src/controllers/notificationController.spec.js b/src/controllers/notificationController.spec.js new file mode 100644 index 000000000..2a7662122 --- /dev/null +++ b/src/controllers/notificationController.spec.js @@ -0,0 +1,313 @@ +const notificationController = require('./notificationController'); +const Notification = require('../models/notification'); +const notificationService = require('../services/notificationService'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const makeSut = () => { + const { + getUserNotifications, + getUnreadUserNotifications, + getSentNotifications, + createUserNotification, + deleteUserNotification, + markNotificationAsRead, + } = notificationController(Notification); + + return { + getUserNotifications, + getUnreadUserNotifications, + getSentNotifications, + createUserNotification, + deleteUserNotification, + markNotificationAsRead, + }; +}; + +describe('Notification controller Unit Tests', () => { + beforeEach(() => { + mockReq.params.userId = '65cf6c3706d8ac105827bb2e'; + mockReq.body.requestor.role = 'Administrator'; + mockReq.body.requestor = { + requestorId: '65cf6c3706d8ac105827bb2e', + role: 'Administrator', + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getUserNotifications', () => { + test('Ensures getUserNotifications returns error 400 if userId is not provided', async () => { + const { getUserNotifications } = makeSut(); + const errorMsg = { error: 'User ID is required' }; + mockReq.params.userId = ''; + const response = await getUserNotifications(mockReq, mockRes); + assertResMock(400, errorMsg, response, mockRes); + }); + test('Ensures getUserNotifications returns error 403 if userId does not match requestorId', async () => { + const { getUserNotifications } = makeSut(); + const errorMsg = { error: 'Unauthorized request' }; + mockReq.body.requestor.requestorId = 'differentUserId'; + const response = await getUserNotifications(mockReq, mockRes); + assertResMock(403, errorMsg, response, mockRes); + }); + test('Ensures getUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { + const { getUserNotifications } = makeSut(); + const mockNotifications = [ + { id: '123', message: 'Notification Test 1' }, + { id: '123', message: 'Notification Test 2' }, + { id: '123', message: 'Notification Test 3' }, + ]; + const mockService = jest.fn().mockResolvedValue(mockNotifications); + notificationService.getNotifications = mockService; + const response = await getUserNotifications(mockReq, mockRes); + assertResMock(200, mockNotifications, response, mockRes); + }); + test('Ensures getUserNotifications returns error 500 if there is an internal error while fetching notifications.', async () => { + const { getUserNotifications } = makeSut(); + const errorMsg = { error: 'Internal Error' }; + const mockService = jest.fn().mockRejectedValue(errorMsg); + notificationService.getNotifications = mockService; + const response = await getUserNotifications(mockReq, mockRes); + assertResMock(500, errorMsg, response, mockRes); + }); + }); + + describe('getUnreadUserNotifications', () => { + test('Ensures getUnreadUserNotifications returns error 400 if userId is not provided', async () => { + const { getUnreadUserNotifications } = makeSut(); + const errorMsg = { error: 'User ID is required' }; + mockReq.params.userId = ''; + const response = await getUnreadUserNotifications(mockReq, mockRes); + assertResMock(400, errorMsg, response, mockRes); + }); + test('Ensures getUnreadUserNotifications returns error 403 if userId does not match requestorId', async () => { + const { getUnreadUserNotifications } = makeSut(); + const errorMsg = { error: 'Unauthorized request' }; + mockReq.body.requestor.requestorId = 'differentUserId' + const response = await getUnreadUserNotifications(mockReq, mockRes); + assertResMock(403, errorMsg, response, mockRes); + }); + test('Ensures getUnreadUserNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { + const { getUnreadUserNotifications } = makeSut(); + const mockNotifications = [ + { id: '123', message: 'Notification Test 1' }, + { id: '123', message: 'Notification Test 2' }, + { id: '123', message: 'Notification Test 3' }, + ]; + const mockService = jest.fn().mockResolvedValue(mockNotifications); + notificationService.getUnreadUserNotifications = mockService; + const response = await getUnreadUserNotifications(mockReq, mockRes); + assertResMock(200, mockNotifications, response, mockRes); + }); + test('Ensures getUnreadUserNotifications returns error 500 if there is an internal error while fetching notifications.', async () => { + const { getUnreadUserNotifications } = makeSut(); + const errorMsg = { error: 'Internal Error' }; + const mockService = jest.fn().mockRejectedValue(errorMsg); + notificationService.getUnreadUserNotifications = mockService; + const response = await getUnreadUserNotifications(mockReq, mockRes); + assertResMock(500, errorMsg, response, mockRes); + }); + }); + describe('getSentNotifications', () => { + test('Ensures getSentNotifications returns error 403 if requestor role is neither Administrator or Owner', async () => { + const { getSentNotifications } = makeSut(); + const errorMsg = { error: 'Unauthorized request' }; + mockReq.body.requestor.role = 'randomRole' + const response = await getSentNotifications(mockReq, mockRes); + assertResMock(403, errorMsg, response, mockRes); + }); + test('Ensures getSentNotifications returns 200 and notifications data when notifications are fetched successfully', async () => { + const { getSentNotifications } = makeSut(); + const mockNotifications = []; + const mockService = jest.fn().mockResolvedValue(mockNotifications); + notificationService.getSentNotifications = mockService; + const response = await getSentNotifications(mockReq, mockRes); + assertResMock(200, mockNotifications, response, mockRes); + }); + + test('Ensures getSentNotification returns error 500 if there is an internal error while fetching notifications.', async () => { + const { getSentNotifications } = makeSut(); + const errorMsg = { error: 'Internal Error' }; + const mockService = jest.fn().mockRejectedValue(errorMsg); + notificationService.getSentNotifications = mockService; + const response = await getSentNotifications(mockReq, mockRes); + assertResMock(500, errorMsg, response, mockRes); + }); + }); + describe('createUserNotification', () => { + test('Ensures createUserNotification returns error 403 when requestor role is not Admin or Owner', async () => { + const { createUserNotification } = makeSut(); + const errorMsg = { error: 'Unauthorized request' }; + mockReq.body.requestor.role = 'randomRole' + mockReq.requestor = { + requestorId: '65cf6c3706d8ac105827bb2e', + }; + const response = await createUserNotification(mockReq, mockRes); + assertResMock(403, errorMsg, response, mockRes); + }); + test('Ensures createUserNotification returns error 400 if message or recipient is missing', async () => { + const { createUserNotification } = makeSut(); + const errorMsg = { error: 'Message and recipient are required' }; + mockReq.body = { + requestor: { + role: 'Administrator', + }, + message: '', + recipient: '', + }; + const response = await createUserNotification(mockReq, mockRes); + assertResMock(400, errorMsg, response, mockRes); + }); + test('Ensures createUserNotification returns 200 and notification data when notification is created successfully', async () => { + const { createUserNotification } = makeSut(); + const mockNotification = { + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + sender: '5a7e21f00317bc1538def4b7', + }; + mockReq.body = { + requestor: { + role: 'Administrator', + }, + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + }; + mockReq.requestor = { + requestorId: '5a7e21f00317bc1538def4b7', + }; + + const mockService = jest.fn().mockResolvedValue(mockNotification); + notificationService.createNotification = mockService; + + await createUserNotification(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(mockNotification); + expect(mockService).toHaveBeenCalledWith( + mockReq.requestor.requestorId, + mockReq.body.recipient, + mockReq.body.message, + ); + }); + test('Ensures createUserNotification returns error 500 if there is an internal error while creating a notification.', async () => { + const { createUserNotification } = makeSut(); + mockReq.body.requestor = { + requestorId: '65cf6c3706d8ac105827bb2e', + role: 'Administrator', + }; + notificationService.createNotification = jest + .fn() + .mockRejectedValue({ error: 'Internal Error' }); + await createUserNotification(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); + }); + }); + describe('deleteUserNotification', () => { + test('Ensures deleteUserNotification returns error 403 when requestor role is not Admin or Owner', async () => { + const { deleteUserNotification } = makeSut(); + mockReq.body.requestor.role = 'randomRole'; + await deleteUserNotification(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.send).toHaveBeenCalledWith({ error: 'Unauthorized request' }); + }); + test('Ensures deleteUserNotification returns 200 and deletes notification', async () => { + const { deleteUserNotification } = makeSut(); + const mockNotification = { + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + sender: '5a7e21f00317bc1538def4b7', + }; + mockReq.body = { + requestor: { + role: 'Administrator', + }, + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + }; + mockReq.requestor = { + requestorId: '5a7e21f00317bc1538def4b7', + }; + + const mockService = jest.fn().mockResolvedValue(mockNotification); + notificationService.deleteNotification = mockService; + + await deleteUserNotification(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(mockNotification); + expect(mockService).toHaveBeenCalledWith(mockReq.params.notificationId); + }); + test('Ensures deleteUserNotification returns error 500 if there is an internal error while deleting a notification.', async () => { + const { deleteUserNotification } = makeSut(); + mockReq.body.requestor = { + requestorId: '65cf6c3706d8ac105827bb2e', + role: 'Administrator', + }; + notificationService.deleteNotification = jest + .fn() + .mockRejectedValue({ error: 'Internal Error' }); + await deleteUserNotification(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); + }); + }); + describe('markNotificationsAsRead', () => { + test('Ensures markNotificationAsRead returns 400 if recipientId is missing', () => { + const { markNotificationAsRead } = makeSut(); + mockReq.body.requestor.requestorId = ''; + markNotificationAsRead(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.send).toHaveBeenCalledWith({ error: 'Recipient ID is required' }); + }); + test('Ensures markNotificationAsRead returns 200 and marks notification as read', async () => { + const { markNotificationAsRead } = makeSut(); + const mockNotification = { + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + sender: '5a7e21f00317bc1538def4b7', + }; + mockReq.body = { + requestor: { + role: 'Administrator', + }, + message: 'Notification Test', + recipient: '65cf6c3706d8ac105827bb2e', + }; + mockReq.body.requestor = { + requestorId: '5a7e21f00317bc1538def4b7', + }; + mockReq.params = { + notificationId: '12345', + }; + + const mockService = jest.fn().mockResolvedValue(mockNotification); + notificationService.markNotificationAsRead = mockService; + + await markNotificationAsRead(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(mockNotification); + expect(mockService).toHaveBeenCalledWith( + mockReq.params.notificationId, + mockReq.body.requestor.requestorId, + ); + }); + test('Ensures markNotificationAsRead returns 500 if there is an internal error while marking notification as read.', async () => { + const { markNotificationAsRead } = makeSut(); + mockReq.body.requestor = { + requestorId: '65cf6c3706d8ac105827bb2e', + role: 'Administrator', + }; + notificationService.markNotificationAsRead = jest + .fn() + .mockRejectedValue({ error: 'Internal Error' }); + await markNotificationAsRead(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith({ error: 'Internal Error' }); + }); + }); +}); diff --git a/src/controllers/ownerMessageController.spec.js b/src/controllers/ownerMessageController.spec.js new file mode 100644 index 000000000..c56044136 --- /dev/null +++ b/src/controllers/ownerMessageController.spec.js @@ -0,0 +1,139 @@ +const OwnerMessage = require('../models/ownerMessage'); +const ownerMessageController = require('./ownerMessageController'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const makeSut = () => { + const { getOwnerMessage, updateOwnerMessage, deleteOwnerMessage } = + ownerMessageController(OwnerMessage); + return { + getOwnerMessage, + updateOwnerMessage, + deleteOwnerMessage, + }; +}; +const flushPromises = () => new Promise(setImmediate); + +describe('ownerMessageController Unit Tests', () => { + let mockFind; + let mockSave; + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeEach(() => { + mockFind = jest.spyOn(OwnerMessage, 'find'); + mockSave = jest.fn(); + }); + describe('getOwnerMessage', () => { + test('Ensures getOwnerMessage returns status 404 if owner message cant be found', async () => { + const { getOwnerMessage } = makeSut(); + const errorMsg = 'Error occurred when finding owner message'; + mockFind.mockImplementationOnce(() => Promise.reject(errorMsg)); + const response = await getOwnerMessage(mockReq, mockRes); + await flushPromises(); + assertResMock(404, errorMsg, response, mockRes); + }); + test('Ensures getOwnerMessage returns status 200 with new owner message if none exist', async () => { + mockFind.mockResolvedValue([]); + const ownerMessageInstance = new OwnerMessage(); + ownerMessageInstance.set = jest.fn(); + const mockSaveFn = jest.fn().mockResolvedValue(ownerMessageInstance); + + jest.spyOn(OwnerMessage.prototype, 'save').mockImplementation(mockSaveFn); + await makeSut().getOwnerMessage(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith( + expect.objectContaining({ + ownerMessage: expect.objectContaining({ + _id: expect.anything(), + message: '', + standardMessage: '', + }), + }), + ); + expect(mockSaveFn).toHaveBeenCalled(); + }); + + test('Ensures getOwnerMessage returns status 200 with the first owner message if it exists', async () => { + const existingMessage = { message: 'Existing message', standardMessage: 'Standard message' }; + mockFind.mockResolvedValue([existingMessage]); + await makeSut().getOwnerMessage(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith({ ownerMessage: existingMessage }); + }); + }); + describe('updateOwnerMessage', () => { + test('Ensures updateOwnerMessage returns status 403 if requestor is not an owner', async () => { + const { updateOwnerMessage } = makeSut(); + const req = { body: { requestor: { role: 'User' } } }; + const response = await updateOwnerMessage(req, mockRes); + await flushPromises(); + assertResMock(403, 'You are not authorized to create messages!', response, mockRes); + }); + test('Ensures updateOwnerMessage returns status 201 and updates the owner message correctly with custom message', async () => { + const existingMessage = { message: '', standardMessage: '', save: mockSave }; + mockFind.mockResolvedValue([existingMessage]); + const mockReqDup = { + ...mockReq, + body: { + ...mockReq.body, + isStandard: false, + newMessage: 'New custom message', + requestor: { role: 'Owner' }, + }, + }; + await makeSut().updateOwnerMessage(mockReqDup, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(201); + expect(mockRes.send).toHaveBeenCalledWith({ + _serverMessage: 'Update successfully!', + ownerMessage: { standardMessage: '', message: 'New custom message' }, + }); + expect(mockSave).toHaveBeenCalled(); + }); + test('Ensures updateOwnerMessage returns status 500 if an error occurs during the update', async () => { + const errorMsg = 'Error occurred during update'; + mockFind.mockRejectedValue(errorMsg); + const mockReqDup = { ...mockReq, body: { ...mockReq.body, requestor: { role: 'Owner' } } }; + await makeSut().updateOwnerMessage(mockReqDup, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + }); + }); + describe('deleteOwnerMessage', () => { + test('Ensures deleteOwnerMessage returns status 403 if requestor is not an owner', async () => { + const { deleteOwnerMessage } = makeSut(); + mockReq.body.requestor.role = 'notOwner'; + const response = await deleteOwnerMessage(mockReq, mockRes); + await flushPromises(); + assertResMock(403, 'You are not authorized to delete messages!', response, mockRes); + }); + test('Ensures deleteOwnerMessage returns status 200 and deletes the owner message correctly', async () => { + const existingMessage = { + message: 'Existing message', + standardMessage: 'Standard message', + save: mockSave, + }; + const { deleteOwnerMessage } = makeSut(); + mockFind.mockResolvedValue([existingMessage]); + mockReq.body.requestor.role = ''; + await deleteOwnerMessage(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith({ + _serverMessage: 'Delete successfully!', + ownerMessage: existingMessage, + }); + expect(mockSave).toHaveBeenCalled(); + }); + test('Ensures deleteOwnerMessage returns status 500 if an error occurs during the delete', async () => { + const { deleteOwnerMessage } = makeSut(); + const errorMsg = 'Error occurred during delete'; + mockFind.mockRejectedValue(errorMsg); + mockReq.body.requestor.role = 'Owner'; + await deleteOwnerMessage(mockReq, mockRes); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith(errorMsg); + }); + }); +}); diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index f6086d02f..fcf24ce1a 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -7,7 +7,6 @@ const config = require('../config'); const cache = require('../utilities/nodeCache')(); const LOGGER = require('../startup/logger'); - const TOKEN_HAS_SETUP_MESSAGE = 'SETUP_ALREADY_COMPLETED'; const TOKEN_CANCEL_MESSAGE = 'CANCELLED'; const TOKEN_INVALID_MESSAGE = 'INVALID'; @@ -121,24 +120,10 @@ const profileInitialSetupController = function ( ProfileInitialSetupToken, userProfile, Project, - MapLocation + MapLocation, ) { const { JWT_SECRET } = config; - const setMapLocation = async (locationData) => { - const location = new MapLocation(locationData); - - try { - const response = await location.save(); - return response; - } catch (err) { - return { - type: "Error", - message: err.message || "An error occurred while saving the location", - }; - } - }; - /** * Function to handle token generation and email process: - Generates a new token and saves it to the database. @@ -156,16 +141,22 @@ const profileInitialSetupController = function ( const expiration = moment().add(3, 'week'); // Wrap multiple db operations in a transaction const session = await startSession(); - + session.startTransaction(); + try { - const existingEmail = await userProfile.findOne({ - email, - }); + const existingEmail = await userProfile + .findOne({ + email, + }) + .session(session); + if (existingEmail) { + await session.abortTransaction(); + session.endSession(); return res.status(400).send('email already in use'); } - session.startTransaction(); - await ProfileInitialSetupToken.findOneAndDelete({ email }); + + await ProfileInitialSetupToken.findOneAndDelete({ email }).session(session); const newToken = new ProfileInitialSetupToken({ token, @@ -177,7 +168,7 @@ const profileInitialSetupController = function ( createdDate: Date.now(), }); - const savedToken = await newToken.save(); + const savedToken = await newToken.save({ session }); const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); @@ -259,8 +250,112 @@ const profileInitialSetupController = function ( }); if (existingEmail) { - res.status(400).send('email already in use'); - return; + return res.status(400).send('email already in use'); + } + if (foundToken) { + const expirationMoment = moment(foundToken.expiration); + + if (expirationMoment.isAfter(currentMoment)) { + const defaultProject = await Project.findOne({ + projectName: 'Orientation and Initial Setup', + }); + + const newUser = new userProfile(); + newUser.password = req.body.password; + newUser.role = 'Volunteer'; + newUser.firstName = req.body.firstName; + newUser.lastName = req.body.lastName; + newUser.jobTitle = req.body.jobTitle; + newUser.phoneNumber = req.body.phoneNumber; + newUser.bio = ''; + newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; + newUser.weeklycommittedHoursHistory = [ + { + hours: newUser.weeklycommittedHours, + dateChanged: Date.now(), + }, + ]; + newUser.personalLinks = []; + newUser.adminLinks = []; + newUser.teams = Array.from(new Set([])); + newUser.projects = Array.from(new Set([defaultProject])); + newUser.createdDate = Date.now(); + newUser.email = req.body.email; + newUser.weeklySummaries = [{ summary: '' }]; + newUser.weeklySummariesCount = 0; + newUser.weeklySummaryOption = 'Required'; + newUser.mediaUrl = ''; + newUser.collaborationPreference = req.body.collaborationPreference; + newUser.timeZone = req.body.timeZone || 'America/Los_Angeles'; + newUser.location = req.body.location; + newUser.profilePic = req.body.profilePicture; + newUser.permissions = { + frontPermissions: [], + backPermissions: [], + }; + newUser.bioPosted = 'default'; + newUser.privacySettings.email = req.body.privacySettings.email; + newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; + newUser.teamCode = ''; + newUser.isFirstTimelog = true; + newUser.homeCountry = req.body.homeCountry || req.body.location; + + const savedUser = await newUser.save(); + + emailSender( + process.env.MANAGER_EMAIL || 'jae@onecommunityglobal.org', // "jae@onecommunityglobal.org" + `NEW USER REGISTERED: ${savedUser.firstName} ${savedUser.lastName}`, + informManagerMessage(savedUser), + null, + null, + ); + await ProfileInitialSetupToken.findByIdAndDelete(foundToken._id); + + const jwtPayload = { + userid: savedUser._id, + role: savedUser.role, + permissions: savedUser.permissions, + expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), + }; + + const token = jwt.sign(jwtPayload, JWT_SECRET); + + const locationData = { + title: '', + firstName: req.body.firstName, + lastName: req.body.lastName, + jobTitle: req.body.jobTitle, + location: req.body.homeCountry, + isActive: true, + }; + + res.send({ token }).status(200); + + const mapEntryResult = await setMapLocation(locationData); + if (mapEntryResult.type === 'Error') { + console.log(mapEntryResult.message); + } + + const NewUserCache = { + permissions: savedUser.permissions, + isActive: true, + weeklycommittedHours: savedUser.weeklycommittedHours, + createdDate: savedUser.createdDate.toISOString(), + _id: savedUser._id, + role: savedUser.role, + firstName: savedUser.firstName, + lastName: savedUser.lastName, + email: savedUser.email, + }; + + const allUserCache = JSON.parse(cache.getCache('allusers')); + allUserCache.push(NewUserCache); + cache.setCache('allusers', JSON.stringify(allUserCache)); + } else { + return res.status(400).send('Token is expired'); + } + } else { + return res.status(400).send('Invalid token'); } const expirationMoment = moment(foundToken.expiration); @@ -316,6 +411,7 @@ const profileInitialSetupController = function ( newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; newUser.teamCode = ''; newUser.isFirstTimelog = true; + newUser.homeCountry = req.body.homeCountry || req.body.location; const savedUser = await newUser.save(); @@ -336,15 +432,6 @@ const profileInitialSetupController = function ( const jwtToken = jwt.sign(jwtPayload, JWT_SECRET); - const locationData = { - title: '', - firstName: req.body.firstName, - lastName: req.body.lastName, - jobTitle: req.body.jobTitle, - location: req.body.homeCountry, - isActive: true, - }; - res.status(200).send({ token: jwtToken }); await ProfileInitialSetupToken.findOneAndUpdate( { _id: foundToken._id }, @@ -352,11 +439,6 @@ const profileInitialSetupController = function ( { new: true }, ); - const mapEntryResult = await setMapLocation(locationData); - if (mapEntryResult.type === 'Error') { - console.log(mapEntryResult.message); - } - const NewUserCache = { permissions: savedUser.permissions, isActive: true, @@ -390,10 +472,9 @@ const profileInitialSetupController = function ( const foundToken = await ProfileInitialSetupToken.findOne({ token }); if (foundToken) { - res.status(200).send({ userAPIKey: premiumKey }); - } else { - res.status(403).send("Unauthorized Request"); + return res.status(200).send({ userAPIKey: premiumKey }); } + return res.status(403).send('Unauthorized Request'); }; function calculateTotalHours(hoursByCategory) { @@ -407,16 +488,11 @@ const profileInitialSetupController = function ( const getTotalCountryCount = async (req, res) => { try { const users = []; - const results = await userProfile.find( - {}, - "location totalTangibleHrs hoursByCategory" - ); + const results = await userProfile.find({}, 'location totalTangibleHrs hoursByCategory'); results.forEach((item) => { if ( - (item.location?.coords.lat && - item.location?.coords.lng && - item.totalTangibleHrs >= 10) || + (item.location?.coords.lat && item.location?.coords.lng && item.totalTangibleHrs >= 10) || (item.location?.coords.lat && item.location?.coords.lng && calculateTotalHours(item.hoursByCategory) >= 10) @@ -424,22 +500,21 @@ const profileInitialSetupController = function ( users.push(item); } }); - const modifiedUsers = users.map(item => ({ + const modifiedUsers = users.map((item) => ({ location: item.location, })); const mapUsers = await MapLocation.find({}); const combined = [...modifiedUsers, ...mapUsers]; - const countries = combined.map(user => user.location.country); + const countries = combined.map((user) => user.location.country); const totalUniqueCountries = [...new Set(countries)].length; - res.status(200).send({ CountryCount: totalUniqueCountries }); + return res.status(200).send({ CountryCount: totalUniqueCountries }); } catch (error) { - res.status(500).send(`Error: ${error}`); + LOGGER.logException(error, 'Error in getTotalCountryCount'); + return res.status(500).send(`Error: ${error}`); } }; - - /** * Returns a list of setup token in not completed status * @param {*} req HTTP request include requester role information @@ -449,29 +524,36 @@ const profileInitialSetupController = function ( const getSetupInvitation = (req, res) => { const { role } = req.body.requestor; if (role === 'Administrator' || role === 'Owner') { - try{ - ProfileInitialSetupToken - .find({ isSetupCompleted: false }) - .sort({ createdDate: -1 }) - .exec((err, result) => { - // Handle the result - if (err) { - LOGGER.logException(err); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } - return res.status(200).send(result); - }); - } catch (error) { - LOGGER.logException(error); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } + try { + ProfileInitialSetupToken.find({ isSetupCompleted: false }) + .sort({ createdDate: -1 }) + .exec((err, result) => { + // Handle the result + if (err) { + LOGGER.logException(err); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } + return res.status(200).send(result); + }); + } catch (error) { + LOGGER.logException(error); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } } else { return res.status(403).send('You are not authorized to get setup history.'); } }; /** - * Cancel the setup token + * Cancel the setup token * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully cancelled * @returns @@ -481,33 +563,40 @@ const profileInitialSetupController = function ( const { token } = req.body; if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken - .findOneAndUpdate( - { token }, - { isCancelled: true }, - (err, result) => { - if (err) { - LOGGER.logException(err); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - } - sendEmailWithAcknowledgment( - result.email, - 'One Community: Your Profile Setup Link Has Been Deactivated', - sendCancelLinkMessage(), - ); - return res.status(200).send(result); - }, - ); - } catch (error) { + ProfileInitialSetupToken.findOneAndUpdate( + { token }, + { isCancelled: true }, + (err, result) => { + if (err) { + LOGGER.logException(err); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + } + sendEmailWithAcknowledgment( + result.email, + 'One Community: Your Profile Setup Link Has Been Deactivated', + sendCancelLinkMessage(), + ); + return res.status(200).send(result); + }, + ); + } catch (error) { LOGGER.logException(error); - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); } } else { res.status(403).send('You are not authorized to cancel setup invitation.'); } }; - /** - * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. + /** + * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. * @param {*} req HTTP request include requester role information * @param {*} res HTTP response include whether the setup invitation record is successfully refreshed * @returns updated result of the setup invitation record. @@ -518,30 +607,37 @@ const profileInitialSetupController = function ( if (role === 'Administrator' || role === 'Owner') { try { - ProfileInitialSetupToken - .findOneAndUpdate( + ProfileInitialSetupToken.findOneAndUpdate( { token }, { expiration: moment().add(3, 'week'), isCancelled: false, }, ) - .then((result) => { - const { email } = result; - const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; - sendEmailWithAcknowledgment( - email, - 'Invitation Link Refreshed: Complete Your One Community Profile Setup', - sendRefreshedLinkMessage(link), - ); - return res.status(200).send(result); - }) - .catch((err) => { - LOGGER.logException(err); - res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); - }); + .then((result) => { + const { email } = result; + const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; + sendEmailWithAcknowledgment( + email, + 'Invitation Link Refreshed: Complete Your One Community Profile Setup', + sendRefreshedLinkMessage(link), + ); + return res.status(200).send(result); + }) + .catch((err) => { + LOGGER.logException(err); + res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); + }); } catch (error) { - return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + return res + .status(500) + .send( + 'Internal Error: Please retry. If the problem persists, please contact the administrator', + ); } } else { return res.status(403).send('You are not authorized to refresh setup invitation.'); @@ -581,4 +677,4 @@ const profileInitialSetupController = function ( }; }; -module.exports = profileInitialSetupController; \ No newline at end of file +module.exports = profileInitialSetupController; diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index b149416b7..72522ba80 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -1,45 +1,53 @@ + /* eslint-disable quotes */ /* eslint-disable arrow-parens */ -const mongoose = require("mongoose"); -const timeentry = require("../models/timeentry"); -const userProfile = require("../models/userProfile"); -const userProject = require("../helpers/helperModels/userProjects"); -const { hasPermission } = require("../utilities/permissions"); -const escapeRegex = require("../utilities/escapeRegex"); -const cache = require("../utilities/nodeCache")(); +const mongoose = require('mongoose'); +const timeentry = require('../models/timeentry'); +const task = require('../models/task'); +const wbs = require('../models/wbs'); +const userProfile = require('../models/userProfile'); +const { hasPermission } = require('../utilities/permissions'); +const escapeRegex = require('../utilities/escapeRegex'); +const logger = require('../startup/logger'); +const cache = require('../utilities/nodeCache')(); const projectController = function (Project) { - const getAllProjects = function (req, res) { - Project.find({}, "projectName isActive category modifiedDatetime") - .sort({ modifiedDatetime: -1 }) - .then((results) => { - res.status(200).send(results); - }) - .catch((error) => res.status(404).send(error)); + const getAllProjects = async function (req, res) { + try { + const projects = await Project.find( + { isArchived: { $ne: true } }, + 'projectName isActive category modifiedDatetime membersModifiedDatetime', + ).sort({ modifiedDatetime: -1 }); + res.status(200).send(projects); + } catch (error) { + logger.logException(error); + res.status(404).send('Error fetching projects. Please try again.'); + } }; const deleteProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "deleteProject"))) { - res - .status(403) - .send({ error: "You are not authorized to delete projects." }); + if (!(await hasPermission(req.body.requestor, 'deleteProject'))) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { projectId } = req.params; Project.findById(projectId, (error, record) => { if (error || !record || record === null || record.length === 0) { - res.status(400).send({ error: "No valid records found" }); + + res.status(400).send({ error: 'No valid records found' }); + return; } - // find if project has any time entries associated with it - timeentry.find({ projectId: record._id }, "_id").then((timeentries) => { + + timeentry.find({ projectId: record._id }, '_id').then((timeentries) => { if (timeentries.length > 0) { res.status(400).send({ error: - "This project has associated time entries and cannot be deleted. Consider inactivaing it instead.", + 'This project has associated time entries and cannot be deleted. Consider inactivaing it instead.', }); + } else { const removeprojectfromprofile = userProfile .updateMany({}, { $pull: { projects: record._id } }) @@ -48,10 +56,11 @@ const projectController = function (Project) { Promise.all([removeprojectfromprofile, removeproject]) .then( + res.status(200).send({ - message: - "Project successfully deleted and user profiles updated.", - }) + message: 'Project successfully deleted and user profiles updated.', + }), + ) .catch((errors) => { res.status(400).send(errors); @@ -64,98 +73,154 @@ const projectController = function (Project) { }; const postProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "postProject"))) { - res - .status(403) - .send({ error: "You are not authorized to create new projects." }); - return; + + if (!(await hasPermission(req.body.requestor, 'postProject'))) { + return res.status(401).send('You are not authorized to create new projects.'); } - if (!req.body.projectName || !req.body.isActive) { - res.status(400).send({ - error: "Project Name and active status are mandatory fields.", - }); - return; + if (!req.body.projectName) { + return res.status(400).send('Project Name is mandatory fields.'); } - Project.find({ - projectName: { $regex: escapeRegex(req.body.projectName), $options: "i" }, - }).then((result) => { - if (result.length > 0) { - res.status(400).send({ - error: `Project Name must be unique. Another project with name ${result.projectName} already exists. Please note that project names are case insensitive.`, - }); + try { + const projectWithRepeatedName = await Project.find({ + projectName: { + $regex: escapeRegex(req.body.projectName), + $options: 'i', + }, + }); + if (projectWithRepeatedName.length > 0) { + res + .status(400) + .send( + `Project Name must be unique. Another project with name ${req.body.projectName} already exists. Please note that project names are case insensitive.`, + ); return; } const _project = new Project(); + const now = new Date(); _project.projectName = req.body.projectName; - _project.category = req.body.projectCategory || "Unspecified"; - _project.isActive = req.body.isActive; - _project.createdDatetime = Date.now(); - _project.modifiedDatetime = Date.now(); - - _project - .save() - .then((results) => res.status(201).send(results)) - .catch((error) => res.status(500).send({ error })); - }); + _project.category = req.body.projectCategory; + _project.isActive = true; + _project.createdDatetime = now; + _project.modifiedDatetime = now; + const savedProject = await _project.save(); + return res.status(200).send(savedProject); + } catch (error) { + logger.logException(error); + res.status(400).send('Error creating project. Please try again.'); + } }; const putProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putProject"))) { - res - .status(403) - .send("You are not authorized to make changes in the projects."); + if (!(await hasPermission(req.body.requestor, "editProject"))) { + if (!(await hasPermission(req.body.requestor, 'putProject'))) { + res.status(403).send('You are not authorized to make changes in the projects.'); + return; + } + } + const { projectName, category, isActive, _id: projectId, isArchived } = req.body; + const sameNameProejct = await Project.find({ + projectName, + _id: { $ne: projectId }, + }); + if (sameNameProejct.length > 0) { + res.status(400).send('This project name is already taken'); return; } - - const { projectId } = req.params; - Project.findById(projectId, (error, record) => { - if (error || record === null) { - res.status(400).send("No valid records found"); + const session = await mongoose.startSession(); + session.startTransaction(); + try { + const targetProject = await Project.findById(projectId); + if (!targetProject) { + res.status(400).send('No valid records found'); return; } - - record.projectName = req.body.projectName; - record.category = req.body.category; - record.isActive = req.body.isActive; - record.modifiedDatetime = Date.now(); - - record - .save() - .then((results) => res.status(201).send(results._id)) - .catch((errors) => res.status(400).send(errors)); - }); + targetProject.projectName = projectName; + targetProject.category = category; + targetProject.isActive = isActive; + targetProject.modifiedDatetime = Date.now(); + if (isArchived) { + targetProject.isArchived = isArchived; + // deactivate wbs within target project + await wbs.updateMany({ projectId }, { isActive: false }, { session }); + // deactivate tasks within affected wbs + const deactivatedwbsIds = await wbs.find({ projectId }, '_id'); + await task.updateMany( + { wbsId: { $in: deactivatedwbsIds } }, + { isActive: false }, + { session }, + ); + // remove project from userprofiles.projects array + await userProfile.updateMany( + { projects: projectId }, + { $pull: { projects: projectId } }, + { session }, + ); + // deactivate timeentry for affected tasks + await timeentry.updateMany({ projectId }, { isActive: false }, { session }); + } + await targetProject.save({ session }); + await session.commitTransaction(); + res.status(200).send(targetProject); + } catch (error) { + await session.abortTransaction(); + logger.logException(error); + res.status(400).send('Error updating project. Please try again.'); + } finally { + session.endSession(); + } }; const getProjectById = function (req, res) { const { projectId } = req.params; - Project.findById(projectId, "-__v -createdDatetime -modifiedDatetime") + Project.findById(projectId, '-__v -createdDatetime -modifiedDatetime') .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .catch((err) => { + logger.logException(err); + res.status(404).send('Error fetching project. Please try again.'); + }); }; - const getUserProjects = function (req, res) { - const { userId } = req.params; + const getUserProjects = async function (req, res) { + try { + const { userId } = req.params; + const user = await userProfile.findById(userId, 'projects'); + if (!user) { + res.status(400).send('Invalid user'); + return; + } + const { projects } = user; + const projectList = await Project.find( + { _id: { $in: projects }, isActive: { $ne: false } }, + '_id projectName category', + ); + const result = projectList + .map((p) => { + p = p.toObject(); + p.projectId = p._id; + delete p._id; + return p; + }) + .sort((p1, p2) => { + if (p1.projectName.toLowerCase() < p2.projectName.toLowerCase()) return -1; + if (p1.projectName.toLowerCase() > p2.projectName.toLowerCase()) return 1; + return 0; + }); + res.status(200).send(result); + } catch (error) { + logger.logException(error); + res.status(400).send('Error fetching projects. Please try again.'); + } - userProject - .findById(userId) - .then((results) => { - res.status(200).send(results.projects); - }) - .catch((error) => { - res.status(400).send(error); - }); }; const assignProjectToUsers = async function (req, res) { // verify requestor is administrator, projectId is passed in request params and is valid mongoose objectid, and request body contains an array of users - if (!(await hasPermission(req.body.requestor, "assignProjectToUsers"))) { - res - .status(403) - .send({ error: "You are not authorized to perform this operation" }); + if (!(await hasPermission(req.body.requestor, 'assignProjectToUsers'))) { + res.status(403).send('You are not authorized to perform this operation'); return; } @@ -165,16 +230,22 @@ const projectController = function (Project) { !req.body.users || req.body.users.length === 0 ) { - res.status(400).send({ error: "Invalid request" }); + + res.status(400).send('Invalid request'); return; } - + const now = new Date(); // verify project exists - - Project.findById(req.params.projectId) + Project.findByIdAndUpdate( + req.params.projectId, + { + $set: { membersModifiedDatetime: now }, + }, + { new: true }, + ) .then((project) => { if (!project || project.length === 0) { - res.status(400).send({ error: "Invalid project" }); + res.status(400).send('Invalid project'); return; } const { users } = req.body; @@ -186,7 +257,7 @@ const projectController = function (Project) { if (cache.hasCache(`user-${userId}`)) { cache.removeCache(`user-${userId}`); } - if (operation === "Assign") { + if (operation === 'Assign') { assignlist.push(userId); } else { unassignlist.push(userId); @@ -194,16 +265,10 @@ const projectController = function (Project) { }); const assignPromise = userProfile - .updateMany( - { _id: { $in: assignlist } }, - { $addToSet: { projects: project._id } } - ) + .updateMany({ _id: { $in: assignlist } }, { $addToSet: { projects: project._id } }) .exec(); const unassignPromise = userProfile - .updateMany( - { _id: { $in: unassignlist } }, - { $pull: { projects: project._id } } - ) + .updateMany({ _id: { $in: unassignlist } }, { $pull: { projects: project._id } }) .exec(); Promise.all([assignPromise, unassignPromise]) @@ -214,21 +279,24 @@ const projectController = function (Project) { res.status(500).send({ error }); }); }) - .catch((error) => { - res.status(500).send({ error }); + .catch((err) => { + logger.logException(err); + res.status(500).send('Error fetching project. Please try again.'); }); }; - const getprojectMembership = function (req, res) { + const getprojectMembership = async function (req, res) { const { projectId } = req.params; if (!mongoose.Types.ObjectId.isValid(projectId)) { - res.status(400).send({ error: "Invalid request" }); + res.status(400).send('Invalid request'); return; } + const getId = await hasPermission(req.body.requestor, 'getProjectMembers'); + userProfile .find( { projects: projectId }, - "_id firstName lastName isActive profilePic" + { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: getId }, ) .sort({ firstName: 1, lastName: 1 }) .then((results) => { diff --git a/src/controllers/reportsController.js b/src/controllers/reportsController.js index baca9ca13..ba629897f 100644 --- a/src/controllers/reportsController.js +++ b/src/controllers/reportsController.js @@ -1,10 +1,80 @@ +/* eslint-disable consistent-return */ const mongoose = require('mongoose'); -const reporthelper = require('../helpers/reporthelper')(); +const reporthelperClosure = require('../helpers/reporthelper'); +const overviewReportHelperClosure = require('../helpers/overviewReportHelper'); const { hasPermission } = require('../utilities/permissions'); const UserProfile = require('../models/userProfile'); -const userhelper = require('../helpers/userHelper')(); const reportsController = function () { + const overviewReportHelper = overviewReportHelperClosure(); + const reporthelper = reporthelperClosure(); + /** + * Aggregates all the data needed for the volunteer stats page + * # Active volunteers + * # New volunteers + * # Deactivated volunteers + * # Badges awarded + * Location data aggregation + * Weekly anniversaries + * Blue square stats + * In teams stats + */ + const getVolunteerStatsData = async (req, res) => { + const { startDate, endDate } = req.query; + if (!startDate || !endDate) { + return res.status(400).send({ msg: 'Please provide a start and end date' }); + } + const isoStartDate = new Date(startDate); + const isoEndDate = new Date(endDate); + + try { + const [ + volunteerNumberStats, + volunteerHoursStats, + totalHoursWorked, + tasksStats, + workDistributionStats, + roleDistributionStats, + usersInTeamStats, + // blueSquareStats, + anniversaryStats, + totalBadgesAwarded, + totalActiveTeams, + userLocations, + ] = await Promise.all([ + overviewReportHelper.getVolunteerNumberStats(isoStartDate, isoEndDate), + overviewReportHelper.getHoursStats(isoStartDate, isoEndDate), + overviewReportHelper.getTotalHoursWorked(isoStartDate, isoEndDate), + overviewReportHelper.getTasksStats(isoStartDate, isoEndDate), + overviewReportHelper.getWorkDistributionStats(isoStartDate, isoEndDate), + overviewReportHelper.getRoleDistributionStats(), + overviewReportHelper.getTeamMembersCount(), + // overviewReportHelper.getBlueSquareStats(startDate, endDate), + overviewReportHelper.getAnniversaries(startDate, endDate), + overviewReportHelper.getTotalBadgesAwardedCount(startDate, endDate), + overviewReportHelper.getTotalActiveTeamCount(), + overviewReportHelper.getMapLocations(), + ]); + res.status(200).send({ + volunteerNumberStats, + volunteerHoursStats, + totalHoursWorked, + tasksStats, + workDistributionStats, + roleDistributionStats, + usersInTeamStats, + // blueSquareStats, + anniversaryStats, + totalBadgesAwarded, + totalActiveTeams, + userLocations, + }); + } catch (err) { + console.log(err); + res.status(500).send({ msg: 'Error occured while fetching data. Please try again!' }); + } + }; + const getWeeklySummaries = async function (req, res) { if (!(await hasPermission(req.body.requestor, 'getWeeklySummaries'))) { res.status(403).send('You are not authorized to view all users'); @@ -17,7 +87,205 @@ const reportsController = function () { const summaries = reporthelper.formatSummaries(results); res.status(200).send(summaries); }) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); + }; + + /** + * Gets the Volunteer Role Stats, it contains + * 1. 4+ members team count + * 2. Total badges awarded count + * 3. Number of users celebrating their anniversary + * 4. Number of members in team and not in team, with percentage + * 5. Number of active and inactive users + * + * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) + * @param {*} res + */ + const getVolunteerStats = async function (req, res) { + try { + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).send('Please provide startDate and endDate'); + return; + } + + // 1. 4+ members team count + const fourPlusMembersTeamCount = await overviewReportHelper.getFourPlusMembersTeamCount(); + + // 2. Total badges awarded count + const badgeCountQuery = await overviewReportHelper.getTotalBadgesAwardedCount( + startDate, + endDate, + ); + const badgeAwardedCount = badgeCountQuery.length > 0 ? badgeCountQuery[0].badgeCollection : 0; + + // 3. Number of users celebrating their anniversary + const anniversaryCountQuery = await overviewReportHelper.getAnniversaryCount( + startDate, + endDate, + ); + const anniversaryCount = + anniversaryCountQuery.length > 0 ? anniversaryCountQuery[0].anniversaryCount : 0; + + // 4. Number of members in team and not in team, with percentage + const teamMembersCount = await overviewReportHelper.getTeamMembersCount(); + + // 5. Number of active and inactive users + const activeInactiveUsersCount = await overviewReportHelper.getActiveInactiveUsersCount(); + + const volunteerStats = { + fourPlusMembersTeamCount, + badgeAwardedCount, + anniversaryCount, + teamMembersCount, + activeInactiveUsersCount, + }; + + res.status(200).json(volunteerStats); + } catch (error) { + res.status(404).send(error); + } + }; + + /** + * Gets the Volunteer Hours Stats, it groups the users based on the number of hours they have logged + * Every ten hours is a group, so 0-9 hours, 10-19 hours, 20-29 hours, and finally 60+ hours + * It also groups users based off the percentage of their weeklycommittedHours worked for the current and previous week. + * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) + * @param {*} res + */ + const getVolunteerHoursStats = async function (req, res) { + try { + const { startDate, endDate, lastWeekStartDate, lastWeekEndDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).send('Please provide startDate and endDate'); + return; + } + + const volunteerHoursStats = await overviewReportHelper.getVolunteerHoursStats( + startDate, + endDate, + lastWeekStartDate, + lastWeekEndDate, + ); + res.status(200).json(volunteerHoursStats); + } catch (error) { + console.log(error); + res.status(404).send(error); + } + }; + + /** + * Gets the Volunteer Role Stats, it contains + * 1. 4+ members team count + * 2. Total badges awarded count + * 3. Number of users celebrating their anniversary + * 4. role and count of users + * + * @param {*} req params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) + * @param {*} res + */ + const getVolunteerRoleStats = async function (req, res) { + try { + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).send('Please provide startDate and endDate'); + return; + } + + // 1. 4+ members team count + const fourPlusMembersTeamCount = await overviewReportHelper.getFourPlusMembersTeamCount(); + + // 2. Total badges awarded count + const badgeCountQuery = await overviewReportHelper.getTotalBadgesAwardedCount( + startDate, + endDate, + ); + const badgeAwardedCount = badgeCountQuery.length > 0 ? badgeCountQuery[0].badgeCollection : 0; + + // 3. Number of users celebrating their anniversary + const anniversaryCountQuery = await overviewReportHelper.getAnniversaryCount( + startDate, + endDate, + ); + const anniversaryCount = + anniversaryCountQuery.length > 0 ? anniversaryCountQuery[0].anniversaryCount : 0; + + // 4. role and count of users + const roleQuery = await overviewReportHelper.getRoleCount(); + + const roles = roleQuery.map((role) => ({ + role: role._id, + count: role.count, + })); + + const volunteerRoleStats = { + fourPlusMembersTeamCount, + badgeAwardedCount, + anniversaryCount, + roles, + }; + + res.status(200).json(volunteerRoleStats); + } catch (error) { + res.status(404).send(error); + } + }; + + /** + * Gets the Task and Project Stats, it contains + * 1. Total hours logged in tasks + * 2. Total hours logged in projects + * 3. Number of member with tasks assigned + * 4. Number of member without tasks assigned + * @param {*} req: params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) + * @param {*} res + * + */ + const getTaskAndProjectStats = async function (req, res) { + try { + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).send('Please provide startDate and endDate'); + return; + } + + const taskAndProjectStats = await overviewReportHelper.getTaskAndProjectStats( + startDate, + endDate, + ); + res.status(200).json(taskAndProjectStats); + } catch (error) { + res.status(404).send(error); + } + }; + + /** + * Gets the Blue Square Stats, it filters the data based on the startDate and endDate + * @param {*} req: params: startDate, endDate (e.g. 2024-01-14, 2024-01-21) + * @param {*} res + * @todo: Currently, infrigements do not contain a type field, so we are unable to group by type and count the number of infringements. + */ + const getBlueSquareStats = async function (req, res) { + try { + const { startDate, endDate } = req.query; + + if (!startDate || !endDate) { + res.status(400).send('Please provide startDate and endDate'); + return; + } + + const blueSquareStats = await overviewReportHelper.getBlueSquareStats(startDate, endDate); + const blueSquareCount = blueSquareStats.length > 0 ? blueSquareStats[0].infringements : 0; + + res.status(200).json({ msg: { blueSquareCount } }); + } catch (error) { + res.status(404).send(error); + } }; /** @@ -88,6 +356,7 @@ const reportsController = function () { } }; + // eslint-disable-next-line consistent-return const saveReportsRecepients = (req, res) => { const { userid } = req.params; const id = userid; @@ -113,9 +382,7 @@ const reportsController = function () { res.status(404).send('No valid records found'); return; } - res - .status(200) - .send({ message: 'updated user record with getWeeklyReport true' }); + res.status(200).send({ message: 'updated user record with getWeeklyReport true' }); }) .catch((err) => { console.log('error in catch block last:', err); @@ -127,10 +394,16 @@ const reportsController = function () { }; return { + getVolunteerStats, + getVolunteerHoursStats, + getTaskAndProjectStats, getWeeklySummaries, getReportRecipients, deleteReportsRecepients, saveReportsRecepients, + getVolunteerRoleStats, + getBlueSquareStats, + getVolunteerStatsData, }; }; diff --git a/src/controllers/rolePresetsController.js b/src/controllers/rolePresetsController.js index 79d324bbf..daf1c10ab 100644 --- a/src/controllers/rolePresetsController.js +++ b/src/controllers/rolePresetsController.js @@ -1,9 +1,9 @@ -const { hasPermission } = require("../utilities/permissions"); +const helper = require('../utilities/permissions'); const rolePresetsController = function (Preset) { const getPresetsByRole = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putRole"))) { - res.status(403).send("You are not authorized to make changes to roles."); + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { + res.status(403).send('You are not authorized to make changes to roles.'); return; } @@ -18,14 +18,14 @@ const rolePresetsController = function (Preset) { }; const createNewPreset = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putRole"))) { - res.status(403).send("You are not authorized to make changes to roles."); + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { + res.status(403).send('You are not authorized to make changes to roles.'); return; } if (!req.body.roleName || !req.body.presetName || !req.body.permissions) { res.status(400).send({ - error: "roleName, presetName, and permissions are mandatory fields.", + error: 'roleName, presetName, and permissions are mandatory fields.', }); return; } @@ -34,14 +34,15 @@ const rolePresetsController = function (Preset) { preset.roleName = req.body.roleName; preset.presetName = req.body.presetName; preset.permissions = req.body.permissions; - preset.save() - .then(result => res.status(201).send({ newPreset: result, message: 'New preset created' })) - .catch(error => res.status(400).send({ error })); + preset + .save() + .then((result) => res.status(201).send({ newPreset: result, message: 'New preset created' })) + .catch((error) => res.status(400).send({ error })); }; const updatePresetById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putRole"))) { - res.status(403).send("You are not authorized to make changes to roles."); + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { + res.status(403).send('You are not authorized to make changes to roles.'); return; } @@ -51,27 +52,29 @@ const rolePresetsController = function (Preset) { record.roleName = req.body.roleName; record.presetName = req.body.presetName; record.permissions = req.body.permissions; - record.save() - .then(results => res.status(200).send(results)) - .catch(errors => res.status(400).send(errors)); + record + .save() + .then((results) => res.status(200).send(results)) + .catch((errors) => res.status(400).send(errors)); }) - .catch(error => res.status(400).send({ error })); + .catch((error) => res.status(400).send({ error })); }; const deletePresetById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putRole"))) { - res.status(403).send("You are not authorized to make changes to roles."); + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { + res.status(403).send('You are not authorized to make changes to roles.'); return; } const { presetId } = req.params; Preset.findById(presetId) .then((result) => { - result.remove() + result + .remove() .then(res.status(200).send({ message: 'Deleted preset' })) - .catch(error => res.status(400).send({ error })); + .catch((error) => res.status(400).send({ error })); }) - .catch(error => res.status(400).send({ error })); + .catch((error) => res.status(400).send({ error })); }; return { diff --git a/src/controllers/rolePresetsController.spec.js b/src/controllers/rolePresetsController.spec.js new file mode 100644 index 000000000..d009be44e --- /dev/null +++ b/src/controllers/rolePresetsController.spec.js @@ -0,0 +1,444 @@ +const rolePresetsController = require('./rolePresetsController'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const Preset = require('../models/rolePreset'); +const Role = require('../models/role'); +const UserProfile = require('../models/userProfile'); +const helper = require('../utilities/permissions'); + +// Mock the models +jest.mock('../models/role'); +jest.mock('../models/userProfile'); + +const makeSut = () => { + const { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById } = + rolePresetsController(Preset); + return { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById }; +}; + +const flushPromises = () => new Promise(setImmediate); + +describe('rolePresets Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + Role.findOne = jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue({ permissions: ['someOtherPermission'] }), + }); + UserProfile.findById = jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue({ permissions: { frontPermissions: [] } }), + }), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createNewPreset method', () => { + test("Ensure createNewPresets returns 403 if user doesn't have permissions for putRole", async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await createNewPreset(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure createNewPresetsreturns 400 if missing roleName', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + presetName: 'testPreset', + premissions: ['testPremissions'], + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if missing presetName', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + roleName: 'testRole', + premissions: ['testPremissions'], + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if missing permissions', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + roleName: 'testRole', + presetName: 'testPreset', + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if any error when saving new preset', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }, + }; + jest + .spyOn(Preset.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + + const response = await createNewPreset(newMockReq, mockRes); + await flushPromises(); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when saving') }, response, mockRes); + }); + test('Ensure createNewPresets returns 201 if saving new preset successfully', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const data = { + roleName: 'testRoleName', + presetName: 'testPresetName', + premissions: ['somePremissions'], + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }, + }; + jest.spyOn(Preset.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + + const response = await createNewPreset(newMockReq, mockRes); + await flushPromises(); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 201, + { + newPreset: data, + message: 'New preset created', + }, + response, + mockRes, + ); + }); + }); + describe('getPresetsByRole method', () => { + test("Ensure getPresetsByRole returns 403 if user doesn't have permissions for putRole", async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await getPresetsByRole(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure getPresetsByRole returns 400 if error in finding roleName', async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + roleName: 'test roleName', + }, + }; + jest + .spyOn(Preset, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + const response = await getPresetsByRole(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, new Error('Error when finding'), response, mockRes); + }); + test('Ensure getPresetsByRole returns 200 if finding roleName successfully', async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + roleName: 'test roleName', + }, + }; + const data = { + roleName: 'test roleName', + presetName: 'test Presetname2', + permissions: ['read', 'add'], + }; + jest.spyOn(Preset, 'find').mockImplementationOnce(() => Promise.resolve(data)); + const response = await getPresetsByRole(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(200, data, response, mockRes); + }); + }); + describe('updatePresetById method', () => { + test("Ensure updatePresetById returns 403 if user doesn't have permissions for putRole", async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await updatePresetById(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure updatePresetById returns 400 if error in finding by id', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + }; + jest + .spyOn(Preset, 'findById') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); + const response = await updatePresetById(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); + }); + test('Ensure updatePresetById returns 400 if error in saving results', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockRejectedValue(new Error('Error when saving results')); + const response = await updatePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(400, new Error('Error when saving results'), response, mockRes); + }); + test('Ensure updatePresetById returns 200 if updatePreset by id successfully', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const data = { + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }; + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce(data); + const response = await updatePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(200, data, response, mockRes); + }); + }); + describe('deletePresetById method', () => { + test("Ensure deletePresetById returns 403 if user doesn't have permissions for putRole", async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await deletePresetById(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure deletePresetById returns 400 if error in finding by id', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + }; + jest + .spyOn(Preset, 'findById') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); + const response = await deletePresetById(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); + }); + test('Ensure deletePresetById returns 400 if error when removing results', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const removeObj = { remove: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); + jest.spyOn(removeObj, 'remove').mockRejectedValue({ error: 'Error when removing' }); + const response = await deletePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(400, { error: { error: 'Error when removing' } }, response, mockRes); + }); + test('Ensure deletePresetById returns 200 if deleting successfully', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const removeObj = { remove: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); + jest.spyOn(removeObj, 'remove').mockImplementationOnce(() => Promise.resolve(true)); + const response = await deletePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock( + 200, + { + message: 'Deleted preset', + }, + response, + mockRes, + ); + }); + }); +}); diff --git a/src/controllers/rolesController.js b/src/controllers/rolesController.js index 90e820b27..e81e195c9 100644 --- a/src/controllers/rolesController.js +++ b/src/controllers/rolesController.js @@ -1,24 +1,23 @@ -const UserProfile = require("../models/userProfile"); -const cache = require("../utilities/nodeCache")(); -const { hasPermission } = require("../utilities/permissions"); +const UserProfile = require('../models/userProfile'); +const cacheClosure = require('../utilities/nodeCache'); +const { hasPermission } = require('../utilities/permissions'); const rolesController = function (Role) { + const cache = cacheClosure(); const getAllRoles = function (req, res) { Role.find({}) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send({ error })); + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send({ error })); }; const createNewRole = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "postRole"))) { - res.status(403).send("You are not authorized to create new roles."); + if (!(await hasPermission(req.body.requestor, 'postRole'))) { + res.status(403).send('You are not authorized to create new roles.'); return; } if (!req.body.roleName || !req.body.permissions) { - res - .status(400) - .send({ error: "roleName and permissions are mandatory fields." }); + res.status(400).send({ error: 'roleName and permissions are mandatory fields.' }); return; } @@ -27,34 +26,35 @@ const rolesController = function (Role) { role.permissions = req.body.permissions; role.permissionsBackEnd = req.body.permissionsBackEnd; - role.save().then(results => res.status(201).send(results)).catch(err => res.status(500).send({ err })); + role + .save() + .then((results) => res.status(201).send(results)) + .catch((err) => res.status(500).send({ err })); }; const getRoleById = function (req, res) { - const { roleId } = req.params; - Role.findById( - roleId, - ) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send({ error })); -}; + const { roleId } = req.params; + Role.findById(roleId) + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send({ error })); + }; const updateRoleById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putRole"))) { - res.status(403).send("You are not authorized to make changes to roles."); + if (!(await hasPermission(req.body.requestor, 'putRole'))) { + res.status(403).send('You are not authorized to make changes to roles.'); return; } const { roleId } = req.params; if (!req.body.permissions) { - res.status(400).send({ error: "Permissions is a mandatory field" }); + res.status(400).send({ error: 'Permissions is a mandatory field' }); return; } Role.findById(roleId, (error, record) => { if (error || record === null) { - res.status(400).send("No valid records found"); + res.status(400).send('No valid records found'); return; } @@ -62,43 +62,39 @@ const rolesController = function (Role) { record.permissions = req.body.permissions; record.permissionsBackEnd = req.body.permissionsBackEnd; - record.save() - .then(results => res.status(201).send(results)) - .catch(errors => res.status(400).send(errors)); + record + .save() + .then((results) => res.status(201).send(results)) + .catch((errors) => res.status(400).send(errors)); }); }; const deleteRoleById = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "deleteRole"))) { - res.status(403).send("You are not authorized to delete roles."); + if (!(await hasPermission(req.body.requestor, 'deleteRole'))) { + res.status(403).send('You are not authorized to delete roles.'); return; } const { roleId } = req.params; - Role.findById(roleId) - .then(result => ( - result - .remove() - .then(UserProfile - .updateMany({ role: result.roleName }, { role: 'Volunteer' }) - .then(() => { - const isUserInCache = cache.hasCache('allusers'); - if (isUserInCache) { - const allUserData = JSON.parse(cache.getCache('allusers')); - allUserData.forEach((user) => { - if (user.role === result.roleName) { - user.role = 'Volunteer'; - cache.removeCache(`user-${user._id}`); - } - }); - cache.setCache('allusers', JSON.stringify(allUserData)); - } - res.status(200).send({ message: 'Deleted role' }); - }) - .catch(error => res.status(400).send({ error }))) - .catch(error => res.status(400).send({ error })) - )) - .catch(error => res.status(400).send({ error })); + try { + const role = await Role.findById(roleId); + await role.remove(); + await UserProfile.updateMany({ role: role.roleName }, { role: 'Volunteer' }); + + if (cache.hasCache('allusers')) { + const allUserData = JSON.parse(cache.getCache('allusers')); + allUserData.forEach((user) => { + if (user.role === role.roleName) { + user.role = 'Volunteer'; + cache.removeCache(`user-${user._id}`); + } + }); + cache.setCache('allusers', JSON.stringify(allUserData)); + } + res.status(200).send({ message: 'Deleted role' }); + } catch (error) { + res.status(400).send({ error }); + } }; return { diff --git a/src/controllers/rolesController.spec.js b/src/controllers/rolesController.spec.js new file mode 100644 index 000000000..eb8b85e2d --- /dev/null +++ b/src/controllers/rolesController.spec.js @@ -0,0 +1,229 @@ +const Role = require('../models/role'); +const UserProfile = require('../models/userProfile'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +jest.mock('../models/role'); +jest.mock('../models/userProfile'); +jest.mock('../utilities/permissions'); +jest.mock('../utilities/nodeCache'); + +const cacheClosure = require('../utilities/nodeCache'); +const helper = require('../utilities/permissions'); +const rolesController = require('./rolesController'); + +const flushPromises = () => new Promise(setImmediate); + +const mockHasPermission = (value) => + jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); + +const makeMockCache = (method, value) => { + const cacheObject = { + getCache: jest.fn(), + removeCache: jest.fn(), + hasCache: jest.fn(), + setCache: jest.fn(), + }; + + const mockCache = jest.spyOn(cacheObject, method).mockImplementationOnce(() => value); + + cacheClosure.mockImplementationOnce(() => cacheObject); + + return { mockCache, cacheObject }; +}; +const makeSut = () => { + const { getAllRoles, createNewRole, getRoleById, updateRoleById, deleteRoleById } = + rolesController(Role); + return { getAllRoles, createNewRole, getRoleById, updateRoleById, deleteRoleById }; +}; + +describe('rolesController module', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getAllRoles function', () => { + test('Should return 200 and roles on success', async () => { + const { getAllRoles } = makeSut(); + const mockRoles = [{ roleName: 'role', permission: 'permissionTest' }]; + jest.spyOn(Role, 'find').mockResolvedValue(mockRoles); + const response = await getAllRoles(mockReq, mockRes); + assertResMock(200, mockRoles, response, mockRes); + }); + + test('Should return 404 on error', async () => { + const { getAllRoles } = makeSut(); + const error = new Error('Test Error'); + + jest.spyOn(Role, 'find').mockRejectedValue(error); + const response = await getAllRoles(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, { error }, response, mockRes); + }); + }); + + describe('createNewRole function', () => { + test('Should return 403 if user lacks permission', async () => { + const { createNewRole } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + const response = await createNewRole(mockReq, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postRole'); + assertResMock(403, 'You are not authorized to create new roles.', response, mockRes); + }); + + test('Should return 400 if mandatory fields are missing', async () => { + const { createNewRole } = makeSut(); + mockReq.body = {}; + mockHasPermission(true); + const response = await createNewRole(mockReq, mockRes); + assertResMock( + 400, + { error: 'roleName and permissions are mandatory fields.' }, + response, + mockRes, + ); + }); + + test('Should return 201 and the new role on success', async () => { + const { createNewRole } = makeSut(); + mockHasPermission(true); + mockReq.body = { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }; + const mockRole = { + save: jest.fn().mockResolvedValue({ + roleName: 'newRole', + permissions: ['read'], + permissionsBackEnd: ['write'], + }), + }; + jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); + const response = await createNewRole(mockReq, mockRes); + expect(mockRole.save).toHaveBeenCalled(); + assertResMock( + 201, + { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }, + response, + mockRes, + ); + }); + test('Should return 500 on role save error', async () => { + const { createNewRole } = makeSut(); + mockHasPermission(true); + mockReq.body = { roleName: 'newRole', permissions: ['read'], permissionsBackEnd: ['write'] }; + const mockRole = { save: jest.fn().mockRejectedValue(new Error('Save Error')) }; + jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); + const response = await createNewRole(mockReq, mockRes); + await flushPromises(); + assertResMock(500, { err: new Error('Save Error') }, response, mockRes); + }); + }); + + describe('getRoleById function', () => { + test('Should return 200 and the role on success', async () => { + const { getRoleById } = makeSut(); + const mockRole = { roleName: 'role', permissions: ['read'] }; + jest.spyOn(Role, 'findById').mockResolvedValue(mockRole); + const response = await getRoleById(mockReq, mockRes); + assertResMock(200, mockRole, response, mockRes); + }); + + test('Should return 404 on error', async () => { + const { getRoleById } = makeSut(); + const error = new Error('Test Error'); + jest.spyOn(Role, 'findById').mockRejectedValue(error); + const response = await getRoleById(mockReq, mockRes); + await flushPromises(); + assertResMock(404, { error }, response, mockRes); + }); + }); + + describe('updateRoleById function', () => { + test('Should return 403 if user lacks permission', async () => { + const { updateRoleById } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + const response = await updateRoleById(mockReq, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + + test('Should return 400 if mandatory fields are missing', async () => { + const { updateRoleById } = makeSut(); + mockReq.body = {}; + mockHasPermission(true); + const response = await updateRoleById(mockReq, mockRes); + assertResMock(400, { error: 'Permissions is a mandatory field' }, response, mockRes); + }); + + test('Should return 400 if no valid records are found', async () => { + const { updateRoleById } = makeSut(); + mockHasPermission(true); + mockReq.body = { roleId: '5a7e21f00317bc1538def4b7', permissions: ['read'] }; + jest.spyOn(Role, 'findById').mockImplementation((roleId, callback) => callback(null, null)); + const response = await updateRoleById(mockReq, mockRes); + assertResMock(400, 'No valid records found', response, mockRes); + }); + + test('Should return 201 and the updated role on success', async () => { + const { updateRoleById } = makeSut(); + mockHasPermission(true); + mockReq.body = { permissions: ['read'] }; + const mockRole = { + save: jest.fn().mockResolvedValue({ roleName: 'role', permissions: ['read'] }), + }; + jest + .spyOn(Role, 'findById') + .mockImplementation((roleId, callback) => callback(null, mockRole)); + jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); + const response = await updateRoleById(mockReq, mockRes); + expect(mockRole.save).toHaveBeenCalled(); + assertResMock(201, { roleName: 'role', permissions: ['read'] }, response, mockRes); + }); + + test('Should return 500 on role save error', async () => { + const { updateRoleById } = makeSut(); + mockHasPermission(true); + mockReq.body = { permissions: ['read'] }; + const mockRole = { save: jest.fn().mockRejectedValue(new Error('Save Error')) }; + jest + .spyOn(Role, 'findById') + .mockImplementation((roleId, callback) => callback(null, mockRole)); + jest.spyOn(Role.prototype, 'save').mockImplementationOnce(mockRole.save); + const response = await updateRoleById(mockReq, mockRes); + await flushPromises(); + assertResMock(400, new Error('Save Error'), response, mockRes); + }); + }); + + describe('deleteRoleById function', () => { + test('Should return 403 if user lacks permission', async () => { + const { deleteRoleById } = makeSut(); + + const hasPermissionSpy = mockHasPermission(false); + const response = await deleteRoleById(mockReq, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteRole'); + assertResMock(403, 'You are not authorized to delete roles.', response, mockRes); + }); + + test('Should return 200 and the deleted role on success', async () => { + mockHasPermission(true); + + const mockRole = { remove: jest.fn().mockResolvedValue(), roleName: 'role' }; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + const { deleteRoleById } = makeSut(); + jest + .spyOn(cacheObject, 'getCache') + .mockImplementationOnce(() => JSON.stringify([{ role: 'role', _id: '1' }])); + jest.spyOn(Role, 'findById').mockResolvedValue(mockRole); + jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => {}); + jest.spyOn(cacheObject, 'removeCache').mockImplementationOnce(() => {}); + jest.spyOn(UserProfile, 'updateMany').mockResolvedValue(); + + const response = await deleteRoleById(mockReq, mockRes); + expect(mockRole.remove).toHaveBeenCalled(); + expect(hasCacheMock).toHaveBeenCalledWith('allusers'); + expect(cacheObject.getCache).toHaveBeenCalledWith('allusers'); + expect(cacheObject.setCache).toHaveBeenCalled(); + expect(cacheObject.removeCache).toHaveBeenCalled(); + assertResMock(200, { message: 'Deleted role' }, response, mockRes); + }); + }); +}); diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 07ee7b730..1e019ffa1 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -14,6 +14,7 @@ const taskController = function (Task) { let query = { wbsId: { $in: [req.params.wbsId] }, level: { $in: [level] }, + isActive: { $ne: false }, }; const { mother } = req.params; @@ -27,16 +28,16 @@ const taskController = function (Task) { } Task.find(query) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send(error)); + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }; const getWBSId = (req, res) => { const { wbsId } = req.params; WBS.findById(wbsId) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send(error)); + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }; const updateSumUp = ( @@ -82,7 +83,7 @@ const taskController = function (Task) { }; const calculateSubTasks = (level, tasks) => { - const parentTasks = tasks.filter(task => task.level === level); + const parentTasks = tasks.filter((task) => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let sumHoursBest = 0; @@ -141,7 +142,7 @@ const taskController = function (Task) { }; const setDatesSubTasks = (level, tasks) => { - const parentTasks = tasks.filter(task => task.level === level); + const parentTasks = tasks.filter((task) => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let minStartedDate = task.startedDatetime; @@ -173,7 +174,7 @@ const taskController = function (Task) { }; const calculatePriority = (level, tasks) => { - const parentTasks = tasks.filter(task => task.level === level); + const parentTasks = tasks.filter((task) => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let totalNumberPriority = 0; @@ -215,7 +216,7 @@ const taskController = function (Task) { }; const setAssigned = (level, tasks) => { - const parentTasks = tasks.filter(task => task.level === level); + const parentTasks = tasks.filter((task) => task.level === level); parentTasks.forEach((task) => { const childTasks = tasks.filter((taskChild) => taskChild.level === level + 1); let isAssigned = false; @@ -248,9 +249,10 @@ const taskController = function (Task) { $and: [ { $or: [{ taskId: parentId1 }, { parentId1 }, { parentId1: null }] }, { wbsId: { $in: [wbsId] } }, + { isActive: { $ne: false } }, ], }).then((tasks) => { - tasks = [...new Set(tasks.map(item => item))]; + tasks = [...new Set(tasks.map((item) => item))]; for (let lv = 3; lv > 0; lv -= 1) { calculateSubTasks(lv, tasks); setDatesSubTasks(lv, tasks); @@ -306,7 +308,7 @@ const taskController = function (Task) { case 3: // task.num is x.x.x, has two levels of parent (parent: x.x and grandparent: x) task.parentId1 = tasksWithId.find((pTask) => pTask.num === taskNumArr[0])._id; // task of parentId1 has num prop of x task.parentId2 = tasksWithId.find( - pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, + (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, )._id; // task of parentId2 has num prop of x.x task.parentId3 = null; task.mother = task.parentId2; // parent task num prop is x.x @@ -314,10 +316,10 @@ const taskController = function (Task) { case 4: // task.num is x.x.x.x, has three levels of parent (x.x.x, x.x and x) task.parentId1 = tasksWithId.find((pTask) => pTask.num === taskNumArr[0])._id; // x task.parentId2 = tasksWithId.find( - pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, + (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}`, )._id; // x.x task.parentId3 = tasksWithId.find( - pTask => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}.${taskNumArr[2]}`, + (pTask) => pTask.num === `${taskNumArr[0]}.${taskNumArr[1]}.${taskNumArr[2]}`, )._id; // x.x.x task.mother = task.parentId3; // parent task num prop is x.x.x break; @@ -466,7 +468,7 @@ const taskController = function (Task) { }); Promise.all([saveTask, saveWbs, saveProject]) - .then(results => res.status(201).send(results[0])) + .then((results) => res.status(201).send(results[0])) .catch((errors) => { res.status(400).send(errors); }); @@ -490,7 +492,7 @@ const taskController = function (Task) { task .save() .then() - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); }); // level 2 @@ -506,7 +508,7 @@ const taskController = function (Task) { childTask1 .save() .then(true) - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); // level 3 Task.find({ parentId: { $in: [childTask1._id] } }) @@ -521,7 +523,7 @@ const taskController = function (Task) { childTask2 .save() .then(true) - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); // level 4 Task.find({ parentId: { $in: [childTask2._id] } }) @@ -536,19 +538,19 @@ const taskController = function (Task) { childTask3 .save() .then(true) - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); }); } }) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); }); } }) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); }); } }) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); }); res.status(200).send(true); @@ -602,7 +604,7 @@ const taskController = function (Task) { Promise.all(queries) .then(() => res.status(200).send('Success!')) - .catch(err => res.status(400).send(err)); + .catch((err) => res.status(400).send(err)); }); }; @@ -646,7 +648,7 @@ const taskController = function (Task) { Promise.all([removeChildTasks, updateMotherChildrenQty]) .then(() => res.status(200).send({ message: 'Task successfully deleted' })) // no need to resetNum(taskId, mother); - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); }; const deleteTaskByWBS = async (req, res) => { @@ -709,7 +711,7 @@ const taskController = function (Task) { { ...req.body, modifiedDatetime: Date.now() }, ) .then(() => res.status(201).send()) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); }; const swap = async function (req, res) { @@ -750,18 +752,18 @@ const taskController = function (Task) { task1 .save() .then() - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); task2 .save() .then() - .catch(errors => res.status(400).send(errors)); + .catch((errors) => res.status(400).send(errors)); Task.find({ wbsId: { $in: [task1.wbsId] }, }) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send(error)); + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }); }); }; @@ -804,7 +806,7 @@ const taskController = function (Task) { try { Task.find({ wbsId: { $in: [wbsId] } }).then((tasks) => { - tasks = tasks.filter(task => task.level === 1); + tasks = tasks.filter((task) => task.level === 1); tasks.forEach((task) => { updateParents(task.wbsId, task.taskId.toString()); }); @@ -823,26 +825,54 @@ const taskController = function (Task) { const getTasksByUserId = async (req, res) => { const { userId } = req.params; try { - Task.find( - { - 'resources.userID': mongoose.Types.ObjectId(userId), - }, - '-resources.profilePic', - ).then((results) => { - WBS.find({ - _id: { $in: results.map(item => item.wbsId) }, - }).then((WBSs) => { - const resultsWithProjectsIds = results.map((item) => { - item.set( - 'projectId', - WBSs?.find((wbs) => wbs._id.toString() === item.wbsId.toString())?.projectId, - { strict: false }, - ); - return item; - }); - res.status(200).send(resultsWithProjectsIds); + const tasks = await Task.aggregate() + .match({ + resources: { + $elemMatch: { + userID: mongoose.Types.ObjectId(userId), + completedTask: { + $ne: true, + }, + }, + }, + isActive: { + $ne: false, + }, + }) + .lookup({ + from: 'wbs', + localField: 'wbsId', + foreignField: '_id', + as: 'wbs', + }) + .unwind({ + path: '$wbs', + includeArrayIndex: 'string', + preserveNullAndEmptyArrays: true, + }) + .addFields({ + wbsName: '$wbs.wbsName', + projectId: '$wbs.projectId', + }) + .lookup({ + from: 'projects', + localField: 'projectId', + foreignField: '_id', + as: 'project', + }) + .unwind({ + path: '$project', + includeArrayIndex: 'string', + preserveNullAndEmptyArrays: true, + }) + .addFields({ + projectName: '$project.projectName', + }) + .project({ + wbs: 0, + project: 0, }); - }); + res.status(200).send(tasks); } catch (error) { res.status(400).send(error); } @@ -887,7 +917,7 @@ const taskController = function (Task) { { ...req.body, modifiedDatetime: Date.now() }, ) .then(() => res.status(201).send()) - .catch(error => res.status(404).send(error)); + .catch((error) => res.status(404).send(error)); }; const getReviewReqEmailBody = function (name, taskName) { @@ -907,7 +937,7 @@ const taskController = function (Task) { role: { $in: ['Administrator', 'Manager', 'Mentor'] }, }); membership.forEach((member) => { - if (member.teams.some(team => user.teams.includes(team))) { + if (member.teams.some((team) => user.teams.includes(team))) { recipients.push(member.email); } }); diff --git a/src/controllers/taskEditSuggestionController.js b/src/controllers/taskEditSuggestionController.js index 721979d4b..4f9bd6de8 100644 --- a/src/controllers/taskEditSuggestionController.js +++ b/src/controllers/taskEditSuggestionController.js @@ -5,9 +5,18 @@ const wbs = require('../models/wbs'); const taskEditSuggestionController = function (TaskEditSuggestion) { const createOrUpdateTaskEditSuggestion = async function (req, res) { try { - const profile = await userProfile.findById(mongoose.Types.ObjectId(req.body.userId)).select('firstName lastName'); - const wbsProjectId = await wbs.findById(mongoose.Types.ObjectId(req.body.oldTask.wbsId)).select('projectId'); - const projectMembers = await userProfile.find({ projects: mongoose.Types.ObjectId(wbsProjectId.projectId) }, '_id firstName lastName profilePic').sort({ firstName: 1, lastName: 1 }); + const profile = await userProfile + .findById(mongoose.Types.ObjectId(req.body.userId)) + .select('firstName lastName'); + const wbsProjectId = await wbs + .findById(mongoose.Types.ObjectId(req.body.oldTask.wbsId)) + .select('projectId'); + const projectMembers = await userProfile + .find( + { projects: mongoose.Types.ObjectId(wbsProjectId.projectId) }, + '_id firstName lastName profilePic', + ) + .sort({ firstName: 1, lastName: 1 }); const taskIdQuery = { taskId: mongoose.Types.ObjectId(req.body.taskId) }; const update = { @@ -22,7 +31,10 @@ const taskEditSuggestionController = function (TaskEditSuggestion) { projectMembers, }; const options = { - upsert: true, new: true, setDefaultsOnInsert: true, rawResult: true, + upsert: true, + new: true, + setDefaultsOnInsert: true, + rawResult: true, }; const tes = await TaskEditSuggestion.findOneAndUpdate(taskIdQuery, update, options); res.status(200).send(tes); @@ -47,8 +59,16 @@ const taskEditSuggestionController = function (TaskEditSuggestion) { const deleteTaskEditSuggestion = async function (req, res) { try { - await TaskEditSuggestion.deleteOne(req.param.taskEditSuggestionId); - res.status(200).send({ message: `Deleted task edit suggestion with _id: ${req.param.taskEditSuggestionId}` }); + const result = await TaskEditSuggestion.deleteOne(req.param.taskEditSuggestionId); + if (result.deletedCount === 1) { + res.status(200).send({ + message: `Deleted task edit suggestion with _id: ${req.param.taskEditSuggestionId}`, + }); + } else { + res.status(400).send({ + message: `Failed to delete task edit suggestion with _id: ${req.param.taskEditSuggestionId}`, + }); + } } catch (error) { res.status(400).send(error); } diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index fb11120bc..41f515e99 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -228,15 +228,97 @@ const teamcontroller = function (Team) { res.status(500).send(error); }); }; + const updateTeamVisibility = async (req, res) => { + console.log("==============> 9 "); + const { visibility, teamId, userId } = req.body; + + try { + Team.findById(teamId, (error, team) => { + if (error || team === null) { + res.status(400).send('No valid records found'); + return; + } + + const memberIndex = team.members.findIndex(member => member.userId.toString() === userId); + if (memberIndex === -1) { + res.status(400).send('Member not found in the team.'); + return; + } + + team.members[memberIndex].visible = visibility; + team.modifiedDatetime = Date.now(); + + team.save() + .then(updatedTeam => { + // Additional operations after team.save() + const assignlist = []; + const unassignlist = []; + team.members.forEach(member => { + if (member.userId.toString() === userId) { + // Current user, no need to process further + return; + } + + if (visibility) { + assignlist.push(member.userId); + } else { + console.log("Visiblity set to false so removing it"); + unassignlist.push(member.userId); + } + }); + + const addTeamToUserProfile = userProfile + .updateMany({ _id: { $in: assignlist } }, { $addToSet: { teams: teamId } }) + .exec(); + const removeTeamFromUserProfile = userProfile + .updateMany({ _id: { $in: unassignlist } }, { $pull: { teams: teamId } }) + .exec(); + + Promise.all([addTeamToUserProfile, removeTeamFromUserProfile]) + .then(() => { + res.status(200).send({ result: 'Done' }); + }) + .catch((error) => { + res.status(500).send({ error }); + }); + }) + .catch(errors => { + console.error('Error saving team:', errors); + res.status(400).send(errors); + }); + + }); + } catch (error) { + res.status(500).send(`Error updating team visibility: ${ error.message}`); + } + }; + + /** + * Leaner version of the teamcontroller.getAllTeams + * Remove redundant data: members, isActive, createdDatetime, modifiedDatetime. + */ + const getAllTeamCode = async function (req, res) { + Team.find({ isActive: true }, { teamCode: 1, _id: 1, teamName: 1 }) + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => { + // logger.logException(`Fetch team code failed: ${error}`); + res.status(500).send('Fetch team code failed.'); + }); + }; + return { getAllTeams, + getAllTeamCode, getTeamById, postTeam, deleteTeam, putTeam, assignTeamToUsers, getTeamMembership, + updateTeamVisibility, }; }; diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 4d90d6613..5a9b8e973 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1,7 +1,6 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const logger = require('../startup/logger'); -const { getInfringementEmailBody } = require('../helpers/userHelper')(); const UserProfile = require('../models/userProfile'); const Project = require('../models/project'); const Task = require('../models/task'); @@ -10,7 +9,6 @@ const emailSender = require('../utilities/emailSender'); const { hasPermission } = require('../utilities/permissions'); const cacheClosure = require('../utilities/nodeCache'); - const formatSeconds = function (seconds) { const formattedseconds = parseInt(seconds, 10); const values = `${Math.floor( @@ -324,13 +322,83 @@ const addEditHistory = async ( (edit) => moment().tz('America/Los_Angeles').diff(edit.date, 'days') <= 365, ).length; - if (totalRecentEdits >= 3) { + if (totalRecentEdits >= 5) { + const cutOffDate = moment().subtract(1, 'year'); + const recentInfringements = userprofile.infringements.filter((infringement) => + moment(infringement.date).isAfter(cutOffDate), + ); + let modifiedRecentInfringements = 'No Previous Infringements!'; + if (recentInfringements.length) { + modifiedRecentInfringements = recentInfringements + .map((item, index) => { + let enhancedDescription; + if (item.description) { + let sentences = item.description.split('.'); + const dateRegex = + /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; + sentences = sentences.map((sentence) => + sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { + const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; + }), + ); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes( + 'System auto-assigned infringement for editing your time entries', + ) + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + enhancedDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + enhancedDescription = `${item.description}`; + } + } + return `

    ${index + 1}. Date: ${moment(item.date).format( + 'M-D-YYYY', + )}, Description: ${enhancedDescription}

    `; + }) + .join(''); + } + userprofile.infringements.push({ date: moment().tz('America/Los_Angeles'), - description: `${totalRecentEdits} time entry edits in the last calendar year`, + description: `System auto-assigned infringement for editing your time entries <${totalRecentEdits}> times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty. + time entry edits in the last calendar year`, }); - const infringementNotificationEmail = ` + const infringementNotificationToAdminEmailBody = `

    ${userprofile.firstName} ${userprofile.lastName} (${userprofile.email}) was issued a blue square for editing their time entries ${totalRecentEdits} times within the last calendar year. @@ -340,28 +408,37 @@ const addEditHistory = async (

    `; - const emailInfringement = { - date: moment().tz('America/Los_Angeles').format('MMMM-DD-YY'), - description: `You edited your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.`, - }; + const infringementNotificationToUserEmailBody = `Dear ${userprofile.firstName} ${userprofile.lastName}, +

    Oops, it looks like you chose to edit your time entries too many times and you’ve managed to get a blue square.

    +

    Date Assigned: ${moment().tz('America/Los_Angeles').format('M-D-YYYY')}

    \ +

    Description: System auto-assigned infringement for editing your time entries ${totalRecentEdits} times within the last 365 days, exceeding the limit of 4 times per year you can edit them without penalty.

    +

    Total Infringements: This is your ${moment + .localeData() + .ordinal(recentInfringements.length)} blue square of 5.

    +

    Thank you,

    +

    One Community

    + +         +
    +

    ADMINISTRATIVE DETAILS:

    +

    Start Date: ${moment(userprofile.startDate).utc().format('M-D-YYYY')}

    +

    Role: ${userprofile.role}

    +

    Title: ${userprofile.userTitle || 'Volunteer'}

    +

    Previous Blue Square Reasons:

    + ${modifiedRecentInfringements}`; pendingEmailCollection.push( emailSender.bind( null, 'onecommunityglobal@gmail.com', `${userprofile.firstName} ${userprofile.lastName} was issued a blue square for for editing a time entry ${totalRecentEdits} times`, - infringementNotificationEmail, + infringementNotificationToAdminEmailBody, ), emailSender.bind( null, userprofile.email, - "You've been issued a blue square for editing your time entry", - getInfringementEmailBody( - userprofile.firstName, - userprofile.lastName, - emailInfringement, - userprofile.infringements.length, - ), + 'You’ve been issued a blue square for editing your time entries too many times', + infringementNotificationToUserEmailBody, ), ); } @@ -389,34 +466,29 @@ const updateTaskIdInTimeEntry = async (id, timeEntry) => { Object.assign(timeEntry, { taskId, wbsId, projectId }); }; - - /** * Controller for timeEntry */ const timeEntrycontroller = function (TimeEntry) { - /** - * Helper func: Check if this is the first time entry for the given user id - * - * @param {Mongoose.ObjectId} personId - * @returns - */ -const checkIsUserFirstTimeEntry = async (personId) => { - try { - const timeEntry = await TimeEntry.findOne({ - personId, - }); - if (timeEntry) { - return false; + * Helper func: Check if this is the first time entry for the given user id + * + * @param {Mongoose.ObjectId} personId + * @returns + */ + const checkIsUserFirstTimeEntry = async (personId) => { + try { + const timeEntry = await TimeEntry.findOne({ + personId, + }); + if (timeEntry) { + return false; + } + } catch (error) { + throw new Error(`Failed to check user with id ${personId} on time entry`); } - } catch (error) { - throw new Error( - `Failed to check user with id ${personId} on time entry`, - ); - } - return true; -}; + return true; + }; /** * Post a time entry @@ -431,6 +503,13 @@ const checkIsUserFirstTimeEntry = async (personId) => { result.status(400).send({ error: 'Bad request' }); }; + const isPostingForSelf = req.body.personId === req.body.requestor.requestorId; + const canPostTimeEntriesForOthers = await hasPermission(req.body.requestor, 'postTimeEntry'); + if (!isPostingForSelf && !canPostTimeEntriesForOthers) { + res.status(403).send({ error: 'You do not have permission to post time entries for others' }); + return; + } + switch (req.body.entryType) { case 'person': if (!mongoose.Types.ObjectId.isValid(req.body.personId) || isInvalid) returnErr(res); @@ -474,47 +553,50 @@ const checkIsUserFirstTimeEntry = async (personId) => { const userprofile = await UserProfile.findById(timeEntry.personId); - // if the time entry is tangible, update the tangible hours in the user profile - if (timeEntry.isTangible) { - // update the total tangible hours in the user profile and the hours by category - updateUserprofileTangibleIntangibleHrs(timeEntry.totalSeconds, 0, userprofile); - updateUserprofileCategoryHrs( - null, - null, - timeEntry.projectId, - timeEntry.totalSeconds, - userprofile, - ); - // if the time entry is related to a task, update the task hoursLogged - if (timeEntry.taskId) { - updateTaskLoggedHours( - timeEntry.taskId, - 0, - timeEntry.taskId, + if (userprofile) { + // if the time entry is tangible, update the tangible hours in the user profile + if (timeEntry.isTangible) { + // update the total tangible hours in the user profile and the hours by category + updateUserprofileTangibleIntangibleHrs(timeEntry.totalSeconds, 0, userprofile); + updateUserprofileCategoryHrs( + null, + null, + timeEntry.projectId, timeEntry.totalSeconds, userprofile, - session, - pendingEmailCollection, ); + // if the time entry is related to a task, update the task hoursLogged + if (timeEntry.taskId) { + updateTaskLoggedHours( + timeEntry.taskId, + 0, + timeEntry.taskId, + timeEntry.totalSeconds, + userprofile, + session, + pendingEmailCollection, + ); + } + } else { + // if the time entry is intangible, just update the intangible hours in the userprofile + updateUserprofileTangibleIntangibleHrs(0, timeEntry.totalSeconds, userprofile); } - } else { - // if the time entry is intangible, just update the intangible hours in the userprofile - updateUserprofileTangibleIntangibleHrs(0, timeEntry.totalSeconds, userprofile); } // Replace the isFirstTimelog checking logic from the frontend to the backend // Update the user start date to current date if this is the first time entry (Weekly blue square assignment related) const isFirstTimeEntry = await checkIsUserFirstTimeEntry(timeEntry.personId); - if(isFirstTimeEntry) { + if (isFirstTimeEntry) { userprofile.isFirstTimelog = false; userprofile.startDate = now; } await timeEntry.save({ session }); - await userprofile.save({ session }); - - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); + if (userprofile) { + await userprofile.save({ session }); + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); + } await session.commitTransaction(); pendingEmailCollection.forEach((emailHandler) => emailHandler()); @@ -569,16 +651,6 @@ const checkIsUserFirstTimeEntry = async (personId) => { const isSameDayTimeEntry = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === newDateOfWork; const isSameDayAuthUserEdit = isForAuthUser && isSameDayTimeEntry; - const isRequestorAdminLikeRole = ['Owner', 'Administrator'].includes(req.body.requestor.role); - const hasEditTimeEntryPermission = await hasPermission(req.body.requestor, 'editTimeEntry'); - - const canEdit = isSameDayAuthUserEdit || isRequestorAdminLikeRole || hasEditTimeEntryPermission; - - - if (!canEdit) { - const error = 'Unauthorized request'; - return res.status(403).send({ error }); - } const session = await mongoose.startSession(); session.startTransaction(); @@ -616,7 +688,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { dateOfWork: initialDateOfWork, } = timeEntry; - const initialProjectId = initialProjectIdObject.toString(); + const initialProjectId = initialProjectIdObject ? initialProjectIdObject.toString() : null; const initialTaskId = initialTaskIdObject ? initialTaskIdObject.toString() : null; // Check if any of the fields have changed @@ -624,11 +696,61 @@ const checkIsUserFirstTimeEntry = async (personId) => { const tangibilityChanged = initialIsTangible !== newIsTangible; const timeChanged = initialTotalSeconds !== newTotalSeconds; const dateOfWorkChanged = initialDateOfWork !== newDateOfWork; + const isTimeModified = newTotalSeconds !== timeEntry.totalSeconds; + const isDescriptionModified = newNotes !== timeEntry.notes; + + + const canEditTimeEntryTime = await hasPermission(req.body.requestor, 'editTimeEntryTime'); + const canEditTimeEntryDescription = await hasPermission(req.body.requestor, 'editTimeEntryDescription'); + const canEditTimeEntryDate = await hasPermission(req.body.requestor, 'editTimeEntryDate'); + const canEditTimeEntryIsTangible = (isForAuthUser + ? (await hasPermission(req.body.requestor, 'toggleTangibleTime')) + : (await hasPermission(req.body.requestor, 'editTimeEntryToggleTangible'))); + + const isNotUsingAPermission = + (!canEditTimeEntryTime && isTimeModified) || + (!canEditTimeEntryDate && dateOfWorkChanged); + + // Time + if ( + !isSameDayAuthUserEdit && + isTimeModified && + !canEditTimeEntryTime + ) { + const error = `You do not have permission to edit the time entry time`; + return res.status(403).send({ error }); + } + + // Description + if ( + !isSameDayAuthUserEdit && + isDescriptionModified && + !canEditTimeEntryDescription + ) { + const error = `You do not have permission to edit the time entry description`; + return res.status(403).send({ error }); + } + + // Date + if (dateOfWorkChanged && !canEditTimeEntryDate) { + const error = `You do not have permission to edit the time entry date`; + return res.status(403).send({ error }); + } + + // Tangible Time + if ( + tangibilityChanged && + canEditTimeEntryIsTangible + ) { + const error = `You do not have permission to edit the time entry isTangible`; + return res.status(403).send({ error }); + } + timeEntry.notes = newNotes; timeEntry.totalSeconds = newTotalSeconds; timeEntry.isTangible = newIsTangible; timeEntry.lastModifiedDateTime = moment().utc().toISOString(); - timeEntry.projectId = mongoose.Types.ObjectId(newProjectId); + if (newProjectId) timeEntry.projectId = mongoose.Types.ObjectId(newProjectId); timeEntry.wbsId = newWbsId ? mongoose.Types.ObjectId(newWbsId) : null; timeEntry.taskId = newTaskId ? mongoose.Types.ObjectId(newTaskId) : null; timeEntry.dateOfWork = moment(newDateOfWork).format('YYYY-MM-DD'); @@ -636,129 +758,128 @@ const checkIsUserFirstTimeEntry = async (personId) => { // now handle the side effects in task and userprofile if certain fields have changed const userprofile = await UserProfile.findById(personId); - if (tangibilityChanged) { - // if tangibility changed - // tangiblity change usually only happens by itself via tangibility checkbox, - // and it can't be changed by user directly (except for owner-like roles) - // but here the other changes are also considered here for completeness - // change from tangible to intangible - if (initialIsTangible) { - // subtract initial logged hours from old task (if not null) - updateTaskLoggedHours( - initialTaskId, - initialTotalSeconds, - null, - null, - userprofile, - session, - pendingEmailCollection, - ); - // subtract initial logged hours from userprofile totalTangibleHrs and add new logged hours to userprofile totalIntangibleHrs - updateUserprofileTangibleIntangibleHrs( - -initialTotalSeconds, - newTotalSeconds, - userprofile, - ); - - // if project is changed, update userprofile hoursByCategory - if (projectChanged) { - updateUserprofileCategoryHrs( - initialProjectIdObject, + if (userprofile) { + if (tangibilityChanged) { + // if tangibility changed + // tangiblity change usually only happens by itself via tangibility checkbox, + // and it can't be changed by user directly (except for owner-like roles) + // but here the other changes are also considered here for completeness + // change from tangible to intangible + if (initialIsTangible) { + // subtract initial logged hours from old task (if not null) + updateTaskLoggedHours( + initialTaskId, initialTotalSeconds, null, null, userprofile, + session, + pendingEmailCollection, ); + // subtract initial logged hours from userprofile totalTangibleHrs and add new logged hours to userprofile totalIntangibleHrs + updateUserprofileTangibleIntangibleHrs( + -initialTotalSeconds, + newTotalSeconds, + userprofile, + ); + + // if project is changed, update userprofile hoursByCategory + if (projectChanged) { + updateUserprofileCategoryHrs( + initialProjectIdObject, + initialTotalSeconds, + null, + null, + userprofile, + ); + } + } else { + // from intangible to tangible + updateTaskLoggedHours( + null, + null, + newTaskId, + newTotalSeconds, + userprofile, + session, + pendingEmailCollection, + ); + updateUserprofileTangibleIntangibleHrs( + initialTotalSeconds, + -newTotalSeconds, + userprofile, + ); + if (projectChanged) { + updateUserprofileCategoryHrs(null, null, newProjectId, newTotalSeconds, userprofile); + } } - } else { - // from intangible to tangible + // make sure all hours are positive + validateUserprofileHours(userprofile); + } else if (initialIsTangible) { + // if tangibility is not changed, + // when timeentry remains tangible, this is usually when timeentry is edited by user in the same day or by owner-like roles + + // it doesn't matter if task is changed or not, just update taskLoggedHours and userprofile totalTangibleHours with new and old task ids updateTaskLoggedHours( - null, - null, + initialTaskId, + initialTotalSeconds, newTaskId, newTotalSeconds, userprofile, session, pendingEmailCollection, ); - updateUserprofileTangibleIntangibleHrs( - initialTotalSeconds, - -newTotalSeconds, - userprofile, - ); + // when project is also changed if (projectChanged) { - updateUserprofileCategoryHrs(null, null, newProjectId, newTotalSeconds, userprofile); + updateUserprofileCategoryHrs( + initialProjectIdObject, + initialTotalSeconds, + newProjectId, + newTotalSeconds, + userprofile, + ); + validateUserprofileHours(userprofile); } - } - // make sure all hours are positive - validateUserprofileHours(userprofile); - } else if (initialIsTangible) { - // if tangibility is not changed, - // when timeentry remains tangible, this is usually when timeentry is edited by user in the same day or by owner-like roles - - // it doesn't matter if task is changed or not, just update taskLoggedHours and userprofile totalTangibleHours with new and old task ids - updateTaskLoggedHours( - initialTaskId, - initialTotalSeconds, - newTaskId, - newTotalSeconds, - userprofile, - session, - pendingEmailCollection, - ); - // when project is also changed - if (projectChanged) { - updateUserprofileCategoryHrs( - initialProjectIdObject, - initialTotalSeconds, - newProjectId, - newTotalSeconds, - userprofile, - ); - validateUserprofileHours(userprofile); - } - // if time or dateOfWork is changed - if (timeChanged || dateOfWorkChanged) { - const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; - updateUserprofileTangibleIntangibleHrs(timeDiffInSeconds, 0, userprofile); - notifyEditByEmail( - userprofile, - req.body.requestor.requestorId, - initialTotalSeconds, - newTotalSeconds, - initialDateOfWork, - newDateOfWork, - ); - // Update edit history - if ( - !isRequestorAdminLikeRole && - !hasEditTimeEntryPermission && - isSameDayAuthUserEdit && - isGeneralEntry - ) { - addEditHistory( + // if time or dateOfWork is changed + if (timeChanged || dateOfWorkChanged) { + const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; + updateUserprofileTangibleIntangibleHrs(timeDiffInSeconds, 0, userprofile); + notifyEditByEmail( userprofile, + req.body.requestor.requestorId, initialTotalSeconds, newTotalSeconds, initialDateOfWork, newDateOfWork, - pendingEmailCollection, ); + // Update edit history + if (isNotUsingAPermission && isSameDayAuthUserEdit && isGeneralEntry) { + addEditHistory( + userprofile, + initialTotalSeconds, + newTotalSeconds, + initialDateOfWork, + newDateOfWork, + pendingEmailCollection, + ); + } } + } else { + // when timeentry is intangible before and after change, + // just update timeEntry and the intangible hours in userprofile, + // no need to update task/userprofile + const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; + updateUserprofileTangibleIntangibleHrs(0, timeDiffInSeconds, userprofile); } - } else { - // when timeentry is intangible before and after change, - // just update timeEntry and the intangible hours in userprofile, - // no need to update task/userprofile - const timeDiffInSeconds = newTotalSeconds - initialTotalSeconds; - updateUserprofileTangibleIntangibleHrs(0, timeDiffInSeconds, userprofile); } await timeEntry.save({ session }); - await userprofile.save({ session }); + if (userprofile) { + await userprofile.save({ session }); - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); + } pendingEmailCollection.forEach((emailHandler) => emailHandler()); await session.commitTransaction(); @@ -793,17 +914,17 @@ const checkIsUserFirstTimeEntry = async (personId) => { } const { personId, totalSeconds, dateOfWork, projectId, taskId, isTangible } = timeEntry; - const isForAuthUser = personId.toString() === req.body.requestor.requestorId; + const isForAuthUser = personId + ? personId.toString() === req.body.requestor.requestorId + : false; const isSameDayTimeEntry = moment().tz('America/Los_Angeles').format('YYYY-MM-DD') === dateOfWork; const isSameDayAuthUserDelete = isForAuthUser && isSameDayTimeEntry; - const isRequestorAdminLikeRole = ['Owner', 'Administrator'].includes(req.body.requestor.role); const hasDeleteTimeEntryPermission = await hasPermission( req.body.requestor, 'deleteTimeEntry', ); - const canDelete = - isSameDayAuthUserDelete || isRequestorAdminLikeRole || hasDeleteTimeEntryPermission; + const canDelete = isSameDayAuthUserDelete || hasDeleteTimeEntryPermission; if (!canDelete) { res.status(403).send({ error: 'Unauthorized request' }); return; @@ -811,23 +932,27 @@ const checkIsUserFirstTimeEntry = async (personId) => { const userprofile = await UserProfile.findById(personId); - // Revert this tangible timeEntry of related task's hoursLogged - if (isTangible) { - updateUserprofileTangibleIntangibleHrs(-totalSeconds, 0, userprofile); - updateUserprofileCategoryHrs(projectId, totalSeconds, null, null, userprofile); - // if the time entry is related to a task, update the task hoursLogged - if (taskId) { - updateTaskLoggedHours(taskId, totalSeconds, null, null, userprofile, session); + if (userprofile) { + // Revert this tangible timeEntry of related task's hoursLogged + if (isTangible) { + updateUserprofileTangibleIntangibleHrs(-totalSeconds, 0, userprofile); + updateUserprofileCategoryHrs(projectId, totalSeconds, null, null, userprofile); + // if the time entry is related to a task, update the task hoursLogged + if (taskId) { + updateTaskLoggedHours(taskId, totalSeconds, null, null, userprofile, session); + } + } else { + updateUserprofileTangibleIntangibleHrs(0, -totalSeconds, userprofile); } - } else { - updateUserprofileTangibleIntangibleHrs(0, -totalSeconds, userprofile); } - await userprofile.save({ session }); await timeEntry.remove({ session }); + if (userprofile) { + await userprofile.save({ session }); - // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time - removeOutdatedUserprofileCache(userprofile._id.toString()); + // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time + removeOutdatedUserprofileCache(userprofile._id.toString()); + } await session.commitTransaction(); res.status(200).send({ message: 'Successfully deleted' }); @@ -865,6 +990,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { entryType: { $in: ['default', null] }, personId: userId, dateOfWork: { $gte: fromdate, $lte: todate }, + isActive: { $ne: false }, }).sort('-lastModifiedDateTime'); const results = await Promise.all( @@ -872,6 +998,18 @@ const checkIsUserFirstTimeEntry = async (personId) => { timeEntry = { ...timeEntry.toObject() }; const { projectId, taskId } = timeEntry; if (!taskId) await updateTaskIdInTimeEntry(projectId, timeEntry); // if no taskId, then it might be old time entry data that didn't separate projectId with taskId + if (timeEntry.taskId) { + const task = await Task.findById(timeEntry.taskId); + if (task) { + timeEntry.taskName = task.taskName; + } + } + if (timeEntry.projectId) { + const project = await Project.findById(timeEntry.projectId); + if (project) { + timeEntry.projectName = project.projectName; + } + } const hours = Math.floor(timeEntry.totalSeconds / 3600); const minutes = Math.floor((timeEntry.totalSeconds % 3600) / 60); Object.assign(timeEntry, { hours, minutes, totalSeconds: undefined }); @@ -897,7 +1035,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, - ' -createdDateTime', + '-createdDateTime', ) .populate('personId') .populate('projectId') @@ -906,7 +1044,6 @@ const checkIsUserFirstTimeEntry = async (personId) => { .sort({ lastModifiedDateTime: -1 }) .then((results) => { const data = []; - results.forEach((element) => { const record = {}; record._id = element._id; @@ -916,15 +1053,48 @@ const checkIsUserFirstTimeEntry = async (personId) => { record.userProfile = element.personId; record.dateOfWork = element.dateOfWork; [record.hours, record.minutes] = formatSeconds(element.totalSeconds); - record.projectId = element.projectId._id; - record.projectName = element.projectId.projectName; - record.projectCategory = element.projectId.category.toLowerCase(); + record.projectId = element.projectId?._id || null; + record.projectName = element.projectId?.projectName || null; + record.projectCategory = element.projectId?.category.toLowerCase() || null; record.taskId = element.taskId?._id || null; record.taskName = element.taskId?.taskName || null; record.taskClassification = element.taskId?.classification?.toLowerCase() || null; record.wbsId = element.wbsId?._id || null; record.wbsName = element.wbsId?.wbsName || null; + data.push(record); + }); + res.status(200).send(data); + }) + .catch((error) => { + logger.logException(error); + res.status(400).send(error); + }); + }; + const getTimeEntriesForReports = function (req, res) { + const { users, fromDate, toDate } = req.body; + + TimeEntry.find( + { + personId: { $in: users }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + ' -createdDateTime', + ) + .populate('projectId') + + .then((results) => { + const data = []; + + results.forEach((element) => { + const record = {}; + record._id = element._id; + record.isTangible = element.isTangible; + record.personId = element.personId._id; + record.dateOfWork = element.dateOfWork; + [record.hours, record.minutes] = formatSeconds(element.totalSeconds); + record.projectId = element.projectId ? element.projectId._id : ''; + record.projectName = element.projectId ? element.projectId.projectName : ''; data.push(record); }); @@ -950,10 +1120,11 @@ const checkIsUserFirstTimeEntry = async (personId) => { { projectId, dateOfWork: { $gte: fromDate, $lte: todate }, + isActive: { $ne: false }, }, '-createdDateTime -lastModifiedDateTime', ) - .populate('userId') + .populate('personId', 'firstName lastName isActive') .sort({ dateOfWork: -1 }) .then((results) => { res.status(200).send(results); @@ -974,6 +1145,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { entryType: 'person', personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1013,6 +1185,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { entryType: 'project', projectId: { $in: projects }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1050,6 +1223,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { entryType: 'team', teamId: { $in: teams }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1086,6 +1260,7 @@ const checkIsUserFirstTimeEntry = async (personId) => { getLostTimeEntriesForUserList, getLostTimeEntriesForProjectList, getLostTimeEntriesForTeamList, + getTimeEntriesForReports, }; }; diff --git a/src/controllers/timeOffRequestController.spec.js b/src/controllers/timeOffRequestController.spec.js new file mode 100644 index 000000000..9f82c5492 --- /dev/null +++ b/src/controllers/timeOffRequestController.spec.js @@ -0,0 +1,1260 @@ +jest.mock('../utilities/permissions', () => ({ + hasPermission: jest.fn(), // Mocking the hasPermission function directly +})); +jest.mock('../utilities/emailSender'); + +const mongoose = require('mongoose'); +const moment = require('moment-timezone'); +const emailSender = require('../utilities/emailSender'); +const { hasPermission } = require('../utilities/permissions'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const timeOffRequestController = require('./timeOffRequestController'); +const TimeOffRequest = require('../models/timeOffRequest'); +const Team = require('../models/team'); +const UserProfile = require('../models/userProfile'); + +const flushPromises = () => new Promise(setImmediate); + +const { ObjectId } = mongoose.Types; + +const makeSut = () => { + const { + setTimeOffRequest, + getTimeOffRequests, + getTimeOffRequestbyId, + updateTimeOffRequestById, + deleteTimeOffRequestById, + } = timeOffRequestController(TimeOffRequest, Team, UserProfile); + return { + setTimeOffRequest, + getTimeOffRequests, + getTimeOffRequestbyId, + updateTimeOffRequestById, + deleteTimeOffRequestById, + }; +}; + +const getAdminEmailIds = (userProfiles) => { + const rolesToInclude = ['Manager', 'Mentor', 'Administrator']; // describes Admin roles + + return userProfiles + .map((userProfile) => { + if (rolesToInclude.includes(userProfile.role)) { + return userProfile.email; + } + return null; + }) + .filter((email) => email !== null); +}; + +describe('timeOffRequestController.js module', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getTimeOffRequests function', () => { + test('getTimeOffRequests Returns 200 and correctly formatter all time-off requests', async () => { + const { getTimeOffRequests } = makeSut(); + const mockData = [ + { + requestFor: '60c72b2f5f1b2c001c8e4d67', + requests: [ + { + reason: 'Vacation', + startingDate: '2024-06-01T00:00:00Z', + endingDate: '2024-06-07T00:00:00Z', + duration: 1, + }, + { + reason: 'Family Event', + startingDate: '2024-06-15T00:00:00Z', + endingDate: '2024-06-16T00:00:00Z', + duration: 1, + }, + ], + }, + { + requestFor: '60c72b2f5f1b2c001c8e4d68', + requests: [ + { + reason: 'Sick Leave', + startingDate: '2024-06-02T00:00:00Z', + endingDate: '2024-06-13T00:00:00Z', + duration: 2, + }, + ], + }, + { + requestFor: '60c72b2f5f1b2c001c8e4d69', + requests: [ + { + reason: 'Conference', + startingDate: '2024-06-05T00:00:00Z', + endingDate: '2024-06-28T00:00:00Z', + duration: 4, + }, + ], + }, + ]; + + const timeOffRequestAggregateSpy = jest + .spyOn(TimeOffRequest, 'aggregate') + .mockResolvedValueOnce(mockData); + + const expectedFormattedMockData = { + '60c72b2f5f1b2c001c8e4d67': [ + { + reason: 'Vacation', + startingDate: '2024-06-01T00:00:00Z', + endingDate: '2024-06-07T00:00:00Z', + duration: 1, + }, + { + reason: 'Family Event', + startingDate: '2024-06-15T00:00:00Z', + endingDate: '2024-06-16T00:00:00Z', + duration: 1, + }, + ], + '60c72b2f5f1b2c001c8e4d68': [ + { + reason: 'Sick Leave', + startingDate: '2024-06-02T00:00:00Z', + endingDate: '2024-06-13T00:00:00Z', + duration: 2, + }, + ], + '60c72b2f5f1b2c001c8e4d69': [ + { + reason: 'Conference', + startingDate: '2024-06-05T00:00:00Z', + endingDate: '2024-06-28T00:00:00Z', + duration: 4, + }, + ], + }; + + const response = await getTimeOffRequests(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, expectedFormattedMockData, response, mockRes); + expect(timeOffRequestAggregateSpy).toHaveBeenCalled(); + expect(timeOffRequestAggregateSpy).toHaveBeenCalledTimes(1); + }); + + test('getTimeOffRequests Returns 500 if error encountered while aggregating all time-off requests', async () => { + const { getTimeOffRequests } = makeSut(); + const error = { error: 'Error perforing aggregate operation.' }; + const timeOffRequestAggregateSpy = jest + .spyOn(TimeOffRequest, 'aggregate') + .mockRejectedValueOnce(error); + + const response = await getTimeOffRequests(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, error, response, mockRes); + expect(timeOffRequestAggregateSpy).toHaveBeenCalled(); + expect(timeOffRequestAggregateSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getTimeOffRequestbyId function', () => { + test('Returns 404 if time-off request with a particular id is not found', async () => { + const { getTimeOffRequestbyId } = makeSut(); + const mockData = null; + + const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockResolvedValueOnce(mockData); + + const response = await getTimeOffRequestbyId(mockReq, mockRes); + await flushPromises(); + const error = 'Time off request not found'; + assertResMock(404, error, response, mockRes); + expect(findOneSpy).toHaveBeenCalled(); + expect(findOneSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 200 if time-off request with a particular id is found', async () => { + const { getTimeOffRequestbyId } = makeSut(); + const mockData = { + requestFor: 'sd9028_sdas83ink84haso1', + reason: 'Family Gathering.', + startingDate: new Date(2024, 5, 1), + endingDate: new Date(2024, 5, 13), + duration: 2, + }; + + const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockResolvedValueOnce(mockData); + + const response = await getTimeOffRequestbyId(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + expect(findOneSpy).toHaveBeenCalled(); + expect(findOneSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 500 if error occurred while fetching time-off request with an id', async () => { + const { getTimeOffRequestbyId } = makeSut(); + + const error = new Error('Some error occurred.'); + const findOneSpy = jest.spyOn(TimeOffRequest, 'findOne').mockRejectedValueOnce(error); + + const response = await getTimeOffRequestbyId(mockReq, mockRes); + await flushPromises(); + assertResMock(500, error, response, mockRes); + expect(findOneSpy).toHaveBeenCalled(); + expect(findOneSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('deleteTimeOffRequestById function', () => { + test('Returns 403 if user is not authorized', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + // Creating a deep copy of mockReq + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'volunteer'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = []; + mockReqCopy.params.id = '123'; + + const error = 'You are not authorized to set time off requests.'; + + const mockData = null; + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockResolvedValueOnce(mockData); + + hasPermission.mockImplementation(async () => false); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalled(); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 404 if no timeOffRequest exists with the particular Id', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + // Creating a deep copy of mockReq + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'volunteer'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = []; + mockReqCopy.params.id = '123'; + + const error = 'You are not authorized to set time off requests.'; + + const mockData = null; + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockResolvedValueOnce(mockData); + + hasPermission.mockImplementation(async () => false); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalled(); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test('Returns 500 if an error occurs at TimeOffRequest.findById()', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; + mockReqCopy.params.id = '123'; + + const errorMessage = 'Internal Server Error'; + const error = new Error(errorMessage); + + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockImplementationOnce(() => { + throw error; + }); + + hasPermission.mockImplementation(async () => true); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(500, error, response, mockRes); + + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 500 if an error occurs while TimeOffRequest.findByIdAndDelete()', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; + mockReqCopy.params.id = '123'; + + const errorMessage = 'Internal Server Error'; + const error = new Error(errorMessage); + + const mockData = { + requestFor: 'sd9028_sdas83ink84haso1', + reason: 'Family Gathering.', + startingDate: new Date(2024, 5, 1), + endingDate: new Date(2024, 5, 13), + duration: 2, + }; + + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockImplementationOnce(() => mockData); + const findByIdAndDeleteSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndDelete') + .mockImplementationOnce(() => { + throw error; + }); + + hasPermission.mockImplementation(async () => error); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(500, error, response, mockRes); + + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(findByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(findByIdAndDeleteSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 200 on successfully deleting the TimeOffRequest; should not call emailSender as `deleteOwnRequest` is false', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; + mockReqCopy.params.id = '123'; + + const mockData = { + requestFor: 'sd9028_sdas83ink84haso1', + reason: 'Family Gathering.', + startingDate: new Date(2024, 5, 1), + endingDate: new Date(2024, 5, 13), + duration: 2, + }; + + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockImplementationOnce(() => mockData); + const findByIdAndDeleteSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndDelete') + .mockImplementationOnce(() => mockData); + + hasPermission.mockImplementation(async () => true); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(findByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(findByIdAndDeleteSpy).toHaveBeenCalledTimes(1); + + expect(emailSender).toHaveBeenCalledTimes(0); + }); + + test('Returns 200 on successfully deleting the TimeOffRequest; notifyUser calls emailSender once and notifyAdmins does not calls emailSender', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; + mockReqCopy.body.requestor.requestorId = 'sd9028_sdas83ink84haso1'; + mockReqCopy.params.id = '123'; + + const mockData = { + requestFor: 'sd9028_sdas83ink84haso1', + reason: 'Family Gathering.', + startingDate: new Date(2024, 5, 1), + endingDate: new Date(2024, 5, 13), + duration: 2, + }; + + const mockedUserData = { + firstName: 'testUserFirstName', + lastName: 'testUserLastName', + email: 'testUser@testing.com', + }; + + const mockedOwnerAccountEmails = [ + // No owner accounts hence NotifyAdmins sends 0 emails + ]; + + const mockedUserTeams = [ + { + // object represents a team 1 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + { + // object represents a team 2 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + ]; + + const mockedUserProfiles = [ + { role: 'Volunteer', email: 'abc_123' }, + { role: 'Tester', email: 'def_456' }, + { role: 'Developer', email: 'ghi_789' }, + { role: 'Volunteer', email: 'jkl_000' }, + { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, + ]; + + const userProfileFindByIdSpy = jest + .spyOn(UserProfile, 'findById') + .mockResolvedValue(mockedUserData); + + const chaining = { + select: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), + }; + + const userEmails = getAdminEmailIds(mockedUserProfiles); + + const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { + if ('role' in query && query.role === 'Owner') { + return chaining; + } + if ('_id' in query && '$in' in query._id) { + // Mocking the query for _id + return Promise.resolve(mockedUserProfiles); + } + }); + + const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); + + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockResolvedValue(mockData); + + const timeOffRequestFindByIdAndDeleteSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndDelete') + .mockResolvedValue(mockData); + + hasPermission.mockImplementation(async () => true); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledTimes(1); + + expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); + + expect(userProfileFindSpy).toHaveBeenCalledTimes(2); + + expect(teamFindSpy).toHaveBeenCalledTimes(1); + expect(teamFindSpy).toHaveBeenCalledWith({ 'members.userId': mockData.requestFor }); + + expect(emailSender).toHaveBeenCalledTimes( + 1 + mockedOwnerAccountEmails.length + userEmails.length, + ); // just once by notifyUser & notifyAdmins not called + }); + + test('Returns 200 on successfully deleting the TimeOffRequest; notifyUser calls emailSender once and notifyAdmins calls emailSender 5 times', async () => { + const { deleteTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = ['manageTimeOffRequests']; + mockReqCopy.body.requestor.requestorId = 'sd9028_sdas83ink84haso1'; + mockReqCopy.params.id = '123'; + + const mockData = { + requestFor: 'sd9028_sdas83ink84haso1', + reason: 'Family Gathering.', + startingDate: new Date(2024, 5, 1), + endingDate: new Date(2024, 5, 13), + duration: 2, + }; + + const mockedUserData = { + firstName: 'testUserFirstName', + lastName: 'testUserLastName', + email: 'testUser@testing.com', + }; + + const mockedOwnerAccountEmails = [ + // No owner accounts hence NotifyAdmins sends 2 emails + { email: 'temp1@gmail.com' }, + { email: 'temp2@gmail.com' }, + ]; + + const mockedUserTeams = [ + { + // object represents a team 1 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + { + // object represents a team 2 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + ]; + + const mockedUserProfiles = [ + { role: 'Manager', email: 'abc_123' }, + { role: 'Tester', email: 'def_456' }, + { role: 'Developer', email: 'ghi_789' }, + { role: 'Administrator', email: 'jkl_000' }, + { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, + ]; + + const userProfileFindByIdSpy = jest + .spyOn(UserProfile, 'findById') + .mockResolvedValue(mockedUserData); + + const chaining = { + select: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), + }; + + const userEmails = getAdminEmailIds(mockedUserProfiles); + + const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { + if ('role' in query && query.role === 'Owner') { + return chaining; + } + if ('_id' in query && '$in' in query._id) { + // Mocking the query for _id + return Promise.resolve(mockedUserProfiles); + } + }); + + const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); + + const timeOffRequestFindByIdSpy = jest + .spyOn(TimeOffRequest, 'findById') + .mockResolvedValue(mockData); + + const timeOffRequestFindByIdAndDeleteSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndDelete') + .mockResolvedValue(mockData); + + hasPermission.mockImplementation(async () => true); + + const response = await deleteTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdSpy).toHaveBeenCalledTimes(1); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledWith(mockReqCopy.params.id); + expect(timeOffRequestFindByIdAndDeleteSpy).toHaveBeenCalledTimes(1); + + expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); + + expect(userProfileFindSpy).toHaveBeenCalledTimes(2); + + expect(teamFindSpy).toHaveBeenCalledTimes(1); + expect(teamFindSpy).toHaveBeenCalledWith({ 'members.userId': mockData.requestFor }); + + expect(emailSender).toHaveBeenCalledTimes( + 1 + mockedOwnerAccountEmails.length + userEmails.length, + ); // addition of 1 represents emailSender function call by notifyUser Function + }); + }); + + describe('updateTimeOffRequestById function', () => { + test('Returns 403 if user is not authorized', async () => { + const { updateTimeOffRequestById } = makeSut(); + + // Creating a deep copy of mockReq + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'volunteer'; + mockReqCopy.body.requestor.permissions.frontPermissions = []; + mockReqCopy.body.requestor.permissions.backPermissions = []; + mockReqCopy.params.id = '123'; + + const error = 'You are not authorized to set time off requests.'; + + hasPermission.mockImplementation(async () => false); + + const response = await updateTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + }); + + test.each` + duration | startingDate | reason | requestId | expectedMessage + ${'1 week'} | ${new Date('2024-06-8')} | ${'Sick'} | ${null} | ${'bad request'} + ${null} | ${new Date('2024-06-8')} | ${'Injury'} | ${'user123'} | ${'bad request'} + ${'5 week'} | ${null} | ${'Wedding'} | ${'user123'} | ${'bad request'} + ${'7 week'} | ${new Date('2024-06-8')} | ${null} | ${'user123'} | ${'bad request'} + `( + `returns 400 when duration is $duration, startingDate is $startingDate, reason is $reason, and requestId is $requestId`, + async ({ duration, startingDate, reason, requestId, expectedMessage }) => { + const { updateTimeOffRequestById } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.body.requestor.role = 'Administrator'; + mockReqCopy.body.requestor.requestorId = 'user123'; + mockReqCopy.params.id = requestId; + + mockReqCopy.body.duration = duration; + mockReqCopy.body.reason = reason; + mockReqCopy.body.startingDate = startingDate; + mockReqCopy.body.requestId = requestId; + + hasPermission.mockImplementation(async () => true); + + const response = await updateTimeOffRequestById(mockReqCopy, mockRes); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + assertResMock(400, expectedMessage, response, mockRes); + }, + ); + + test('Returns 404 if no timeOffRequest is found', async () => { + const { updateTimeOffRequestById } = makeSut(); + + // Creating a deep copy of mockReq + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.params.id = '123'; + + mockReqCopy.body.requestor = { + ...mockReqCopy.body.requestor, // Preserving existing properties + role: 'Owner', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + }; + + const timeOffDuration = 5; + const timeOffStartingDate = new Date(2024, 5, 12); + const timeOffReason = 'Testing a leave request'; + + moment.tz.setDefault('America/Los_Angeles'); + + const startDate = moment(timeOffStartingDate); + const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); + + const mockUpdateData = { + reason: timeOffReason, + startingDate: startDate.toDate(), + endingDate: endDate.toDate(), + duration: timeOffDuration, + }; + + mockReqCopy.body = { + ...mockReqCopy.body, + duration: timeOffDuration, + startingDate: timeOffStartingDate, + reason: timeOffReason, + }; + + const error = 'Time off request not found'; + + hasPermission.mockImplementation(async () => true); + const timeOffRequestFindByIdAndUpdateSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndUpdate') + .mockImplementationOnce(() => Promise.resolve(null)); + + const response = await updateTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( + mockReqCopy.params.id, + mockUpdateData, + { + new: true, + }, + ); + }); + + test('Returns 200 on successful update operation', async () => { + const { updateTimeOffRequestById } = makeSut(); + + // Creating a deep copy of mockReq + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.params.id = '123'; + + mockReqCopy.body.requestor = { + ...mockReqCopy.body.requestor, + role: 'Owner', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + }; + + const timeOffDuration = 5; + const timeOffStartingDate = new Date(2024, 5, 12); + const timeOffReason = 'Testing a leave request'; + + moment.tz.setDefault('America/Los_Angeles'); + const startDate = moment(timeOffStartingDate); + const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); + + const mockUpdateData = { + reason: timeOffReason, + startingDate: startDate.toDate(), + endingDate: endDate.toDate(), + duration: timeOffDuration, + }; + + mockReqCopy.body = { + ...mockReqCopy.body, + duration: timeOffDuration, + startingDate: timeOffStartingDate, + reason: timeOffReason, + }; + + hasPermission.mockImplementation(async () => true); + const timeOffRequestFindByIdAndUpdateSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndUpdate') + .mockImplementationOnce(() => Promise.resolve(mockUpdateData)); + + const response = await updateTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(200, mockUpdateData, response, mockRes); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( + mockReqCopy.params.id, + mockUpdateData, + { + new: true, + }, + ); + }); + + test('Returns 500 if error occurs with findByIdAndUpdate ', async () => { + const { updateTimeOffRequestById } = makeSut(); + + // Creating a deep copy of the `mockReq` + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + mockReqCopy.params.id = '123'; + + mockReqCopy.body.requestor = { + ...mockReqCopy.body.requestor, + role: 'Owner', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + }; + + const timeOffDuration = 5; + const timeOffStartingDate = new Date(2024, 5, 12); + const timeOffReason = 'Testing a leave request'; + + moment.tz.setDefault('America/Los_Angeles'); + const startDate = moment(timeOffStartingDate); + const endDate = startDate.clone().add(Number(timeOffDuration), 'weeks').subtract(1, 'day'); + + const mockUpdateData = { + reason: timeOffReason, + startingDate: startDate.toDate(), + endingDate: endDate.toDate(), + duration: timeOffDuration, + }; + + mockReqCopy.body = { + ...mockReqCopy.body, + duration: timeOffDuration, + startingDate: timeOffStartingDate, + reason: timeOffReason, + }; + + const error = new Error('Some error occcurred during operation findByIdAndUpdate()'); + + hasPermission.mockImplementation(async () => true); + const timeOffRequestFindByIdAndUpdateSpy = jest + .spyOn(TimeOffRequest, 'findByIdAndUpdate') + .mockRejectedValueOnce(error); + + const response = await updateTimeOffRequestById(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(500, error, response, mockRes); + + expect(hasPermission).toHaveBeenCalledWith( + mockReqCopy.body.requestor, + 'manageTimeOffRequests', + ); + expect(hasPermission).toHaveBeenCalledTimes(1); + + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalled(); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledTimes(1); + expect(timeOffRequestFindByIdAndUpdateSpy).toHaveBeenCalledWith( + mockReqCopy.params.id, + mockUpdateData, + { + new: true, + }, + ); + }); + }); + + describe('setTimeOffRequest function', () => { + test('Returns 403 if the user is not authorised', async () => { + const { setTimeOffRequest } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Volunteer', + requestorId: 'testUser123', + }, + requestFor: 'testUser456', + }; + + hasPermission.mockImplementation(async () => Promise.resolve(false)); + + const error = 'You are not authorized to set time off requests.'; + + const response = await setTimeOffRequest(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + }); + + test('Returns 201 if the time-off request is set successfully; emailSender is not called as setOwnRequested is False', async () => { + // emailSender is not called as setOwnRequested is False + const { setTimeOffRequest } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Administrator', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + requestorId: 'testUser123', + }, + requestFor: 'testUser456', + duration: 1, + startingDate: new Date(2024, 5, 15), + reason: 'Test set time off', + }; + + const mockedResponseDocument = { + requestFor: mockReqCopy.body.requestFor, + duration: mockReqCopy.body.duration, + startingDate: mockReqCopy.body.startDate, + reason: mockReqCopy.body.reason, + endingDate: new Date(2024, 5, 21), + }; + + hasPermission.mockImplementation(async () => Promise.resolve(true)); + const mongooseObjectIdSpy = jest + .spyOn(mongoose.Types, 'ObjectId') + .mockImplementationOnce(() => mockReqCopy.body.requestFor); + const timeOffRequestSaveSpy = jest + .spyOn(TimeOffRequest.prototype, 'save') + .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); + + const response = await setTimeOffRequest(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(201, mockedResponseDocument, response, mockRes); + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + + expect(mongooseObjectIdSpy).toBeCalled(); + expect(mongooseObjectIdSpy).toBeCalledTimes(1); + expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); + + expect(timeOffRequestSaveSpy).toBeCalled(); + expect(timeOffRequestSaveSpy).toBeCalledTimes(1); + + expect(emailSender).toHaveBeenCalledTimes(0); + }); + + test('Returns 201 if the time-off request is set successfully; emailSender is not called as savedRequest is null', async () => { + // emailSender is not called as savedRequest is null + const { setTimeOffRequest } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Administrator', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + requestorId: 'testUser123', + }, + requestFor: 'testUser123', + duration: 1, + startingDate: new Date(2024, 5, 15), + reason: 'Test set time off', + }; + + const mockedResponseDocument = null; + + hasPermission.mockImplementation(async () => Promise.resolve(true)); + const mongooseObjectIdSpy = jest + .spyOn(mongoose.Types, 'ObjectId') + .mockImplementationOnce(() => mockReqCopy.body.requestFor); + const timeOffRequestSaveSpy = jest + .spyOn(TimeOffRequest.prototype, 'save') + .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); + + const response = await setTimeOffRequest(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(201, mockedResponseDocument, response, mockRes); + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + + expect(mongooseObjectIdSpy).toBeCalled(); + expect(mongooseObjectIdSpy).toBeCalledTimes(1); + expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); + + expect(timeOffRequestSaveSpy).toBeCalled(); + expect(timeOffRequestSaveSpy).toBeCalledTimes(1); + + expect(emailSender).toHaveBeenCalledTimes(0); + }); + + test('Returns 201 if the time-off request is set successfully; emailSender is called', async () => { + // emailSender is called as savedRequest is not null and setOwnRequested is True + const { setTimeOffRequest } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Administrator', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + requestorId: 'testUser123', + }, + requestFor: 'testUser123', + duration: 1, + startingDate: new Date(2024, 5, 15), + reason: 'Test set time off', + }; + + mockReqCopy.params.id = 'mockId'; + + const mockedResponseDocument = { + requestFor: mockReqCopy.body.requestFor, + duration: mockReqCopy.body.duration, + startingDate: mockReqCopy.body.startDate, + reason: mockReqCopy.body.reason, + endingDate: new Date(2024, 5, 21), + }; + + const mockedOwnerAccountEmails = [ + // No owner accounts hence NotifyAdmins sends 0 emails + ]; + + const mockedUserTeams = [ + { + // object represents a team 1 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + { + // object represents a team 2 + members: [ + // array represents team members + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3a') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3d') }, + { userId: new ObjectId('60c72b2f9b1d8b3a8c8f8b3e') }, + ], + }, + ]; + + const mockedUserProfiles = [ + { role: 'Volunteer', email: 'abc_123' }, + { role: 'Tester', email: 'def_456' }, + { role: 'Developer', email: 'ghi_789' }, + { role: 'Volunteer', email: 'jkl_000' }, + { role: 'Volunteer', email: 'sd9028_sdas83ink84haso1' }, + ]; + + const mockedUserData = { + firstName: 'testUserFirstName', + lastName: 'testUserLastName', + email: 'testUser@testing.com', + }; + + const userProfileFindByIdSpy = jest + .spyOn(UserProfile, 'findById') + .mockResolvedValue(mockedUserData); + + const chaining = { + select: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(mockedOwnerAccountEmails), + }; + + const userEmails = getAdminEmailIds(mockedUserProfiles); + + const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockImplementation((query) => { + if ('role' in query && query.role === 'Owner') { + return chaining; + } + if ('_id' in query && '$in' in query._id) { + // Mocking the query for _id + return Promise.resolve(mockedUserProfiles); + } + }); + + const teamFindSpy = jest.spyOn(Team, 'find').mockResolvedValue(mockedUserTeams); + + hasPermission.mockImplementation(async () => Promise.resolve(true)); + const mongooseObjectIdSpy = jest + .spyOn(mongoose.Types, 'ObjectId') + .mockImplementationOnce(() => mockReqCopy.body.requestFor); + const timeOffRequestSaveSpy = jest + .spyOn(TimeOffRequest.prototype, 'save') + .mockImplementationOnce(async () => Promise.resolve(mockedResponseDocument)); + + const response = await setTimeOffRequest(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(201, mockedResponseDocument, response, mockRes); + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + + expect(mongooseObjectIdSpy).toBeCalled(); + expect(mongooseObjectIdSpy).toBeCalledTimes(1); + expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); + + expect(timeOffRequestSaveSpy).toBeCalled(); + expect(timeOffRequestSaveSpy).toBeCalledTimes(1); + + expect(userProfileFindByIdSpy).toHaveBeenCalledTimes(2); + + expect(userProfileFindSpy).toHaveBeenCalledTimes(2); + + expect(teamFindSpy).toHaveBeenCalledTimes(1); + expect(teamFindSpy).toHaveBeenCalledWith({ + 'members.userId': mockedResponseDocument.requestFor, + }); + + expect(emailSender).toHaveBeenCalledTimes( + 1 + mockedOwnerAccountEmails.length + userEmails.length, + ); + }); + + test.each` + duration | startingDate | reason | requestFor | expectedMessage + ${null} | ${new Date('2024-06-08')} | ${'Injury'} | ${'user123'} | ${'bad request'} + ${'5 week'} | ${null} | ${'Wedding'} | ${'user123'} | ${'bad request'} + ${'7 week'} | ${new Date('2024-06-08')} | ${null} | ${'user123'} | ${'bad request'} + ${'1 week'} | ${new Date('2024-06-08')} | ${'Sick'} | ${null} | ${'bad request'} + `( + `Return 400 if request body is missing any one of the following $requestFor, $reason, $duration, or $startingDate`, + async ({ duration, startingDate, reason, requestFor, expectedMessage }) => { + const { setTimeOffRequest } = makeSut(); + + hasPermission.mockImplementationOnce(async () => Promise.resolve(true)); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Administrator', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + requestorId: 'testUser123', + }, + requestFor, + duration, + startingDate, + reason, + }; + + const error = expectedMessage; + const response = await setTimeOffRequest(mockReqCopy, mockRes); + + assertResMock(400, error, response, mockRes); + + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + }, + ); + + test('Returns 500 if error occurs while saving time-off request.', async () => { + const { setTimeOffRequest } = makeSut(); + + const mockReqCopy = JSON.parse(JSON.stringify(mockReq)); + + mockReqCopy.body = { + ...mockReqCopy.body, + requestor: { + role: 'Administrator', + permissions: { + frontPermissions: [], + backPermissions: [], + }, + requestorId: 'testUser123', + }, + requestFor: 'testUser456', + duration: 1, + startingDate: new Date(2024, 5, 15), + reason: 'Test set time off', + }; + + const error = 'Error saving the request.'; + + hasPermission.mockImplementation(async () => Promise.resolve(true)); + const mongooseObjectIdSpy = jest + .spyOn(mongoose.Types, 'ObjectId') + .mockImplementationOnce(() => mockReqCopy.body.requestFor); + const timeOffRequestSaveSpy = jest + .spyOn(TimeOffRequest.prototype, 'save') + .mockRejectedValueOnce(error); + + const response = await setTimeOffRequest(mockReqCopy, mockRes); + await flushPromises(); + + assertResMock(500, error, response, mockRes); + + expect(hasPermission).toBeCalled(); + expect(hasPermission).toBeCalledTimes(1); + expect(hasPermission).toBeCalledWith(mockReqCopy.body.requestor, 'manageTimeOffRequests'); + + expect(mongooseObjectIdSpy).toBeCalled(); + expect(mongooseObjectIdSpy).toBeCalledTimes(1); + expect(mongooseObjectIdSpy).toBeCalledWith(mockReqCopy.body.requestFor); + + expect(timeOffRequestSaveSpy).toBeCalled(); + expect(timeOffRequestSaveSpy).toBeCalledTimes(1); + }); + }); +}); diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 3bb268143..f351c6e77 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,11 +1,7 @@ const Team = require('../models/team'); const Project = require('../models/project'); -const cacheClosure = require('../utilities/nodeCache'); -const { getAllTeamCodeHelper } = require("./userProfileController"); const titlecontroller = function (Title) { - const cache = cacheClosure(); - const getAllTitles = function (req, res) { Title.find({}) .then((results) => res.status(200).send(results)) @@ -101,15 +97,11 @@ const titlecontroller = function (Title) { res.status(500).send(error); }); }; - // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. + async function checkTeamCodeExists(teamCode) { try { - if (cache.getCache('teamCodes')) { - const teamCodes = JSON.parse(cache.getCache('teamCodes')); - return teamCodes.includes(teamCode); - } - const teamCodes = await getAllTeamCodeHelper(); - return teamCodes.includes(teamCode); + const team = await Team.findOne({ teamCode }).exec(); + return !!team; } catch (error) { console.error('Error checking if team code exists:', error); throw error; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index efae2c139..a61cf3c43 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -4,7 +4,6 @@ const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); // eslint-disable-next-line import/no-extraneous-dependencies const fetch = require('node-fetch'); - const moment_ = require('moment'); const jwt = require('jsonwebtoken'); const userHelper = require('../helpers/userHelper')(); @@ -14,9 +13,9 @@ const Badge = require('../models/badge'); const yearMonthDayDateValidator = require('../utilities/yearMonthDayDateValidator'); const cacheClosure = require('../utilities/nodeCache'); const followUp = require('../models/followUp'); - +const userService = require('../services/userService'); // const { authorizedUserSara, authorizedUserJae } = process.env; -const authorizedUserSara = `sucheta_mu@test.com`; // To test this code please include your email here +const authorizedUserSara = `nathaliaowner@gmail.com`; // To test this code please include your email here const authorizedUserJae = `jae@onecommunityglobal.org`; const { hasPermission, canRequestorUpdateUser } = require('../utilities/permissions'); @@ -24,7 +23,10 @@ const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); const emailSender = require('../utilities/emailSender'); +const objectUtils = require('../utilities/objectUtils'); + const config = require('../config'); +const { PROTECTED_EMAIL_ACCOUNT } = require('../utilities/constants'); async function ValidatePassword(req, res) { const { userId } = req.params; @@ -74,7 +76,87 @@ async function ValidatePassword(req, res) { } } -const userProfileController = function (UserProfile) { +const sendEmailUponProtectedAccountUpdate = ( + requestorEmail, + requestorFullName, + targetEmail, + action, + logId, +) => { + const updatedDate = moment_().format('MMM-DD-YY'); + const subject = 'One Community: Protected Account Has Been Updated'; + const emailBody = `

    Hi Admin!

    + +

    Protected Account ${targetEmail} is updated by ${requestorEmail}

    + +

    Here are the details for the new ${targetEmail} account:

    +
      +
    • Updated Date: ${updatedDate}
    • +
    • Action: ${action}
    • +
    + +

    Who updated this new account?

    + + +

    If you have any questions or notice any issues, + please investigate further by searching log transaction ID ${logId} in the Sentry .

    + +

    Thank you for your attention to this matter.

    + +

    Sincerely,

    +

    The HGN (and One Community)

    `; + emailSender(targetEmail, subject, emailBody, null, null); +}; + +const auditIfProtectedAccountUpdated = async ( + requestorId, + updatedRecordEmail, + originalRecord, + updatedRecord, + updateDiffPaths, + actionPerformed, +) => { + if (PROTECTED_EMAIL_ACCOUNT.includes(updatedRecordEmail)) { + const requestorProfile = await userService.getUserFullNameAndEmailById(requestorId); + const requestorFullName = requestorProfile + ? requestorProfile.firstName.concat(' ', requestorProfile.lastName) + : 'N/A'; + // remove sensitive data from the original and updated records + let extraData = null; + const updateObject = updatedRecord.toObject(); + if (updateDiffPaths) { + const { originalObj, updatedObj } = objectUtils.returnObjectDifference( + originalRecord, + updateObject, + updateDiffPaths, + ); + const originalObjectString = originalRecord ? JSON.stringify(originalObj) : null; + const updatedObjectString = updatedRecord ? JSON.stringify(updatedObj) : null; + extraData = { + originalObjectString, + updatedObjectString, + }; + } + const logId = logger.logInfo( + `Protected email account updated. Target: ${updatedRecordEmail} + Requestor: ${requestorProfile ? requestorFullName : requestorId}`, + extraData, + ); + + sendEmailUponProtectedAccountUpdate( + requestorProfile?.email, + requestorFullName, + updatedRecordEmail, + actionPerformed, + logId, + ); + } +}; + +const userProfileController = function (UserProfile, Project) { const cache = cacheClosure(); const forbidden = function (res, message) { @@ -84,6 +166,7 @@ const userProfileController = function (UserProfile) { const checkPermission = async function (req, permission) { return helper.hasPermission(req.body.requestor, permission); }; + const getUserProfiles = async function (req, res) { if (!(await checkPermission(req, 'getUserProfiles'))) { forbidden(res, 'You are not authorized to view all users'); @@ -92,7 +175,6 @@ const userProfileController = function (UserProfile) { await UserProfile.find( {}, - '_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate startDate createdDate endDate', ) .sort({ @@ -275,26 +357,26 @@ const userProfileController = function (UserProfile) { const subject = `${process.env.dbName !== 'hgnData_dev' ? '*Main Site* -' : ''}New ${up.role} Role Created`; const emailBody = `

    Hi Admin!

    - +

    New Account Details

    This email is to inform you that ${up.firstName} ${up.lastName} has been created as a new ${up.role} account on the Highest Good Network application.

    - +

    Here are the details for the new ${up.role} account:

    • Name: ${up.firstName} ${up.lastName}
    • Email: ${up.email}
    - +

    Who created this new account?

    - +

    If you have any questions or notice any issues, please investigate further.

    - +

    Thank you for your attention to this matter.

    - +

    Sincerely,

    The HGN A.I. (and One Community)

    `; @@ -331,13 +413,20 @@ const userProfileController = function (UserProfile) { const putUserProfile = async function (req, res) { const userid = req.params.userId; + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userid, + ); + const isRequestorAuthorized = !!( - canRequestorUpdateUser(req.body.requestor.requestorId, userid) && + canEditProtectedAccount && ((await hasPermission(req.body.requestor, 'putUserProfile')) || req.body.requestor.requestorId === userid) ); - if (!isRequestorAuthorized) { + const canManageAdminLinks = await hasPermission(req.body.requestor, 'manageAdminLinks'); + + if (!isRequestorAuthorized && !canManageAdminLinks) { res.status(403).send('You are not authorized to update this user'); return; } @@ -356,6 +445,13 @@ const userProfileController = function (UserProfile) { res.status(404).send('No valid records found'); return; } + + // To keep a copy of the original record if we edit the protected account + let originalRecord = {}; + if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { + originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(record); + // console.log('originalRecord', originalRecord); + } // validate userprofile pic if (req.body.profilePic) { @@ -386,12 +482,10 @@ const userProfileController = function (UserProfile) { 'profilePic', 'firstName', 'lastName', - 'jobTitle', 'phoneNumber', 'bio', 'personalLinks', 'location', - 'profilePic', 'privacySettings', 'weeklySummaries', 'weeklySummariesCount', @@ -401,8 +495,8 @@ const userProfileController = function (UserProfile) { 'totalTangibleHrs', 'totalIntangibleHrs', 'isFirstTimelog', + 'teamCode', 'isVisible', - 'isRehireable', 'bioPosted', ]; @@ -412,16 +506,6 @@ const userProfileController = function (UserProfile) { } }); - // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), - // we need to remove the cache when team code is updated in case of new team code generation - if (req.body.teamCode) { - // remove teamCode cache when new team assigned - if (req.body.teamCode !== record.teamCode) { - cache.removeCache('teamCodes'); - } - record.teamCode = req.body.teamCode; - } - record.lastModifiedDate = Date.now(); // find userData in cache @@ -434,19 +518,29 @@ const userProfileController = function (UserProfile) { userIdx = allUserData.findIndex((users) => users._id === userid); userData = allUserData[userIdx]; } + if (await hasPermission(req.body.requestor, 'updateSummaryRequirements')) { + const summaryFields = ['weeklySummaryNotReq', 'weeklySummaryOption']; + summaryFields.forEach((fieldName) => { + if (req.body[fieldName] !== undefined) { + record[fieldName] = req.body[fieldName]; + } + }); + } + + if (req.body.adminLinks !== undefined && canManageAdminLinks) { + record.adminLinks = req.body.adminLinks; + } + if (await hasPermission(req.body.requestor, 'putUserProfileImportantInfo')) { const importantFields = [ + 'email', 'role', 'isRehireable', 'isActive', - 'adminLinks', - 'isActive', 'weeklySummaries', 'weeklySummariesCount', 'mediaUrl', 'collaborationPreference', - 'weeklySummaryNotReq', - 'weeklySummaryOption', 'categoryTangibleHrs', 'totalTangibleHrs', 'timeEntryEditHistory', @@ -476,7 +570,39 @@ const userProfileController = function (UserProfile) { } if (req.body.projects !== undefined) { - record.projects = Array.from(new Set(req.body.projects)); + const newProjects = req.body.projects.map((project) => project._id.toString()); + + // check if the projects have changed + const projectsChanged = + !record.projects.every((id) => newProjects.includes(id.toString())) || + !newProjects.every((id) => record.projects.map((p) => p.toString()).includes(id)); + + if (projectsChanged) { + // store the old projects for comparison + const oldProjects = record.projects.map((id) => id.toString()); + + // update the projects + record.projects = newProjects.map((id) => mongoose.Types.ObjectId(id)); + + const addedProjects = newProjects.filter((id) => !oldProjects.includes(id)); + const removedProjects = oldProjects.filter((id) => !newProjects.includes(id)); + + const changedProjectIds = [...addedProjects, ...removedProjects].map((id) => + mongoose.Types.ObjectId(id), + ); + + if (changedProjectIds.length > 0) { + const now = new Date(); + Project.updateMany( + { _id: { $in: changedProjectIds } }, + { $set: { membersModifiedDatetime: now } }, + ) + .exec() + .catch((error) => { + console.error('Error updating project membersModifiedDatetime:', error); + }); + } + } } if (req.body.email !== undefined) { @@ -555,7 +681,10 @@ const userProfileController = function (UserProfile) { ) { record.infringements = req.body.infringements; } - + let updatedDiff = null; + if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { + updatedDiff = record.modifiedPaths(); + } record .save() .then((results) => { @@ -568,6 +697,7 @@ const userProfileController = function (UserProfile) { results.role, results.startDate, results.jobTitle[0], + results.weeklycommittedHours, ); res.status(200).json({ _id: record._id, @@ -578,6 +708,15 @@ const userProfileController = function (UserProfile) { allUserData.splice(userIdx, 1, userData); cache.setCache('allusers', JSON.stringify(allUserData)); } + // Log the update of a protected email account + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + originalRecord.email, + originalRecord, + record, + updatedDiff, + 'update', + ); }) .catch((error) => res.status(400).send(error)); }); @@ -585,6 +724,10 @@ const userProfileController = function (UserProfile) { const deleteUserProfile = async function (req, res) { const { option, userId } = req.body; + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userId, + ); if (!(await hasPermission(req.body.requestor, 'deleteUserProfile'))) { res.status(403).send('You are not authorized to delete users'); return; @@ -614,6 +757,18 @@ const userProfileController = function (UserProfile) { const user = await UserProfile.findById(userId); + // Check if the user is protected and if the requestor has permission to delete protected accounts + if (PROTECTED_EMAIL_ACCOUNT.includes(user.email) && !canEditProtectedAccount) { + res.status(403).send({ + error: 'Only authorized users can delete protected accounts', + }); + // + logger.logInfo( + `Unauthorized attempt to delete a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${user.email}`, + ); + return; + } + if (!user) { res.status(400).send({ error: 'Invalid user', @@ -658,12 +813,19 @@ const userProfileController = function (UserProfile) { allUserData.splice(userIdx, 1); cache.setCache('allusers', JSON.stringify(allUserData)); } - + const originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(user); try { await UserProfile.deleteOne({ _id: userId }); // delete followUp for deleted user await followUp.findOneAndDelete({ userId }); res.status(200).send({ message: 'Executed Successfully' }); + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + originalRecord.email, + originalRecord, + null, + 'delete', + ); } catch (err) { res.status(500).send(err); } @@ -737,10 +899,23 @@ const userProfileController = function (UserProfile) { .catch((error) => res.status(404).send(error)); }; - const updateOneProperty = function (req, res) { + const updateOneProperty = async function (req, res) { const { userId } = req.params; const { key, value } = req.body; + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userId, + ); + + if (!canEditProtectedAccount) { + logger.logInfo( + `Unauthorized attempt to update a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, + ); + res.status(403).send('You are not authorized to update this user'); + return; + } + if (key === 'teamCode') { const canEditTeamCode = req.body.requestor.role === 'Owner' || @@ -761,14 +936,29 @@ const userProfileController = function (UserProfile) { return UserProfile.findById(userId) .then((user) => { + let originalRecord = null; + if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { + originalRecord = objectUtils.deepCopyMongooseObjectWithLodash(user); + } user.set({ [key]: value, }); - + let updatedDiff = null; + if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { + updatedDiff = user.modifiedPaths(); + } return user .save() .then(() => { res.status(200).send({ message: 'updated property' }); + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + originalRecord.email, + originalRecord, + user, + updatedDiff, + 'update', + ); }) .catch((error) => res.status(500).send(error)); }) @@ -791,6 +981,19 @@ const userProfileController = function (UserProfile) { }); } // Check if the requestor has the permission to update passwords. + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userId, + ); + + if (!canEditProtectedAccount) { + logger.logInfo( + `Unauthorized attempt to update a protected account. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, + ); + res.status(403).send('You are not authorized to update this user'); + return; + } + const hasUpdatePasswordPermission = await hasPermission(requestor, 'updatePassword'); // if they're updating someone else's password, they need the 'updatePassword' permission. @@ -831,7 +1034,21 @@ const userProfileController = function (UserProfile) { }); return user .save() - .then(() => res.status(200).send({ message: 'updated password' })) + .then(() => { + if (PROTECTED_EMAIL_ACCOUNT.includes(user.email)) { + logger.logInfo( + `Protected email account password updated. Requestor: ${req.body.requestor.requestorId}, Target: ${user.email}`, + ); + } + res.status(200).send({ message: 'updated password' }); + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + user.email, + null, + null, + 'PasswordUpdate', + ); + }) .catch((error) => res.status(500).send(error)); }) .catch((error) => res.status(500).send(error)); @@ -922,12 +1139,46 @@ const userProfileController = function (UserProfile) { }); return; } - if (!(await hasPermission(req.body.requestor, 'changeUserStatus'))) { + + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userId, + ); + + if ( + !((await hasPermission(req.body.requestor, 'changeUserStatus')) && canEditProtectedAccount) + ) { + if (PROTECTED_EMAIL_ACCOUNT.includes(req.body.requestor.email)) { + logger.logInfo( + `Unauthorized attempt to change protected user status. Requestor: ${req.body.requestor.requestorId} Target: ${userId}`, + ); + } res.status(403).send('You are not authorized to change user status'); return; } cache.removeCache(`user-${userId}`); - UserProfile.findById(userId, 'isActive') + const emailReceivers = await UserProfile.find( + { isActive: true, role: { $in: ['Owner'] } }, + '_id isActive role email', + ); + + const recipients = emailReceivers.map((receiver) => receiver.email); + + try { + const findUser = await UserProfile.findById(userId, 'teams'); + findUser.teams.map(async (teamId) => { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + }); + } catch (err) { + logger.logException(err, 'Unexpected error in finding menagement team'); + } + + UserProfile.findById(userId, 'isActive email firstName lastName') .then((user) => { user.set({ isActive: status, @@ -950,6 +1201,20 @@ const userProfileController = function (UserProfile) { allUserData.splice(userIdx, 1, userData); cache.setCache('allusers', JSON.stringify(allUserData)); } + userHelper.sendDeactivateEmailBody( + user.firstName, + user.lastName, + endDate, + user.email, + recipients, + ); + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + user.email, + null, + null, + 'UserStatusUpdate', + ); res.status(200).send({ message: 'status updated', }); @@ -966,11 +1231,17 @@ const userProfileController = function (UserProfile) { const changeUserRehireableStatus = async function (req, res) { const { userId } = req.params; const { isRehireable } = req.body; - + const canEditProtectedAccount = await canRequestorUpdateUser( + req.body.requestor.requestorId, + userId, + ); if (!mongoose.Types.ObjectId.isValid(userId)) { return res.status(400).send({ error: 'Bad Request' }); } - if (!(await hasPermission(req.body.requestor, 'changeUserRehireableStatus'))) { + if ( + !(await hasPermission(req.body.requestor, 'changeUserRehireableStatus')) || + !canEditProtectedAccount + ) { return res.status(403).send('You are not authorized to change rehireable status'); } @@ -1002,6 +1273,13 @@ const userProfileController = function (UserProfile) { if (err) { return res.status(500).send('Error fetching updated user data.'); } + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + verifiedUser.email, + null, + null, + 'UserRehireableStatusUpdate', + ); res.status(200).send({ message: 'Rehireable status updated and verified successfully', isRehireable: verifiedUser.isRehireable, @@ -1058,15 +1336,15 @@ const userProfileController = function (UserProfile) {

    Account Details

    This email is to inform you that a password reset has been executed for an ${user.role} account:

    - + - +

    Account that reset the ${user.role}'s password

    The password reset was made by:

    - +
    • Name: ${requestor.firstName} ${requestor.lastName}
    • Email: ${requestor.email}
    • @@ -1075,7 +1353,7 @@ const userProfileController = function (UserProfile) {

      If you have any questions or need to verify this password reset, please investigate further.

      Thank you for your attention to this matter.

      - +

      Sincerely,

      The HGN A.I. (and One Community)

      `; @@ -1086,6 +1364,13 @@ const userProfileController = function (UserProfile) { res.status(200).send({ message: 'Password Reset', }); + auditIfProtectedAccountUpdated( + req.body.requestor.requestorId, + user.email, + null, + null, + 'UserResetPassword', + ); } catch (error) { res.status(500).send(error); } @@ -1168,15 +1453,11 @@ const userProfileController = function (UserProfile) { const getUserByFullName = (req, res) => { // Sanitize user input and escape special characters const sanitizedFullName = escapeRegExp(req.params.fullName.trim()); - // Create a regular expression to match the sanitized full name, ignoring case const fullNameRegex = new RegExp(sanitizedFullName, 'i'); - + UserProfile.find({ - $or: [ - { firstName: { $regex: fullNameRegex } }, - { lastName: { $regex: fullNameRegex } }, - ], + $or: [{ firstName: { $regex: fullNameRegex } }, { lastName: { $regex: fullNameRegex } }], }) .select('firstName lastName') // eslint-disable-next-line consistent-return @@ -1184,14 +1465,15 @@ const userProfileController = function (UserProfile) { if (users.length === 0) { return res.status(404).send({ error: 'Users Not Found' }); } + res.status(200).send(users); }) .catch((error) => res.status(500).send(error)); }; - function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - } + // function escapeRegExp(string) { + // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // } /** * Authorizes user to be able to add Weekly Report Recipients * @@ -1232,33 +1514,56 @@ const userProfileController = function (UserProfile) { } }; - const getAllTeamCodeHelper = async function () { + const getProjectsByPerson = async function (req, res) { try { - if (cache.hasCache('teamCodes')) { - const teamCodes = JSON.parse(cache.getCache('teamCodes')); - return teamCodes; - } - const distinctTeamCodes = await UserProfile.distinct('teamCode', { - teamCode: { $ne: null } - }); - cache.setCache('teamCodes', JSON.stringify(distinctTeamCodes)); - return distinctTeamCodes; - } catch (error) { - throw new Error('Encountered an error to get all team codes, please try again!'); - } - } + const { name } = req.params; + const match = name.trim().split(' '); + const firstName = match[0]; + const lastName = match[match.length - 1]; + + const query = match[1] + ? { + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + $and: [ + { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, + { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, + ], + }, + ], + } + : { + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + ], + }; - const getAllTeamCode = async function (req, res) { - try { - const distinctTeamCodes = await getAllTeamCodeHelper(); - return res.status(200).send({ message: 'Found', distinctTeamCodes }); - } catch (error) { - return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); - } - } + const userProfile = await UserProfile.find(query); + if (userProfile) { + const allProjects = userProfile + .map((user) => user.projects) + .filter((projects) => projects.length > 0) + .flat(); + if (allProjects.length === 0) { + return res.status(400).send({ message: 'Projects not found' }); + } + return res.status(200).send({ message: 'Found profile and related projects', allProjects }); + } + } catch (error) { + return res.status(500).send({ massage: 'Encountered an error, please try again!' }); + } + }; return { postUserProfile, @@ -1281,8 +1586,7 @@ const userProfileController = function (UserProfile) { getUserByFullName, changeUserRehireableStatus, authorizeUser, - getAllTeamCode, - getAllTeamCodeHelper, + getProjectsByPerson, }; }; diff --git a/src/controllers/wbsController.js b/src/controllers/wbsController.js index 2e325b85b..074cfaf16 100644 --- a/src/controllers/wbsController.js +++ b/src/controllers/wbsController.js @@ -1,20 +1,23 @@ /* eslint-disable quotes */ /* eslint-disable no-unused-vars */ const mongoose = require('mongoose'); -const helper = require('../utilities/permissions'); +const { hasPermission } = require('../utilities/permissions'); const Project = require('../models/project'); const Task = require('../models/task'); const wbsController = function (WBS) { const getAllWBS = function (req, res) { - WBS.find({ projectId: { $in: [req.params.projectId] } }, 'wbsName isActive modifiedDatetime') + WBS.find( + { projectId: { $in: [req.params.projectId] }, isActive: { $ne: false } }, + 'wbsName isActive modifiedDatetime', + ) .sort({ modifiedDatetime: -1 }) - .then(results => res.status(200).send(results)) - .catch(error => res.status(404).send(error)); + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }; const postWBS = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'postWbs'))) { + if (!(await hasPermission(req.body.requestor, 'postWbs'))) { res.status(403).send({ error: 'You are not authorized to create new projects.' }); return; } @@ -42,13 +45,13 @@ const wbsController = function (WBS) { _wbs .save() - .then(results => res.status(201).send(results)) - .catch(error => res.status(500).send({ error })); + .then((results) => res.status(201).send(results)) + .catch((error) => res.status(500).send({ error })); }; const deleteWBS = async function (req, res) { - if (!(await helper.hasPermission(req.body.requestor, 'deleteWbs'))) { - res.status(403).send({ error: 'You are not authorized to delete projects.' }); + if (!(await hasPermission(req.body.requestor, 'deleteWbs'))) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { id } = req.params; @@ -66,15 +69,12 @@ const wbsController = function (WBS) { res.status(400).send(errors); }); }); - // .catch((errors) => { - // res.status(400).send(errors); - // }); }; const getWBS = function (req, res) { - WBS.find() - .then(results => res.status(200).send(results)) - .catch(error => res.status(500).send({ error })); + WBS.find({ isActive: { $ne: false } }) + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(500).send({ error })); }; const getWBSById = function (req, res) { @@ -83,29 +83,7 @@ const wbsController = function (WBS) { .then((results) => { res.status(200).send(results); }) - .catch(error => res.status(404).send(error)); - }; - - const getWBSByUserId = async function (req, res) { - const { userId } = req.params; - try { - const result = await Task.aggregate() - .match({ 'resources.userID': mongoose.Types.ObjectId(userId) }) - .project('wbsId -_id') - .group({ _id: '$wbsId' }) - .lookup({ - from: 'wbs', - localField: '_id', - foreignField: '_id', - as: 'wbs', - }) - .unwind('wbs') - .replaceRoot('wbs'); - - res.status(200).send(result); - } catch (error) { - res.status(404).send(error); - } + .catch((error) => res.status(404).send(error)); }; return { @@ -114,7 +92,6 @@ const wbsController = function (WBS) { getAllWBS, getWBS, getWBSById, - getWBSByUserId, }; }; diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index 77c5ca0b7..f0f69e146 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -7,7 +7,6 @@ const userProfileJobs = () => { const allUserProfileJobs = new CronJob( // '* * * * *', // Comment out for testing. Run Every minute. '1 0 * * 0', // Every Sunday, 1 minute past midnight. - // '30 22 * * 0', // hotfix for 10:30pm async () => { const SUNDAY = 0; // will change back to 0 after fix diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 80422f153..533dbe367 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -2,28 +2,17 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const timeentry = require('../models/timeentry'); -const myTeam = require('./helperModels/myTeam'); const team = require('../models/team'); const { hasPermission } = require('../utilities/permissions'); - const dashboardhelper = function () { const personaldetails = function (userId) { - return userProfile.findById( - userId, - "_id firstName lastName role profilePic badgeCollection" - ); + return userProfile.findById(userId, '_id firstName lastName role profilePic badgeCollection'); }; const getOrgData = async function () { - const pdtstart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .format("YYYY-MM-DD"); - const pdtend = moment() - .tz("America/Los_Angeles") - .endOf("week") - .format("YYYY-MM-DD"); + const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); /** * Previous aggregate pipeline had two issues: @@ -42,43 +31,41 @@ const dashboardhelper = function () { $gte: 1, }, role: { - $ne: "Mentor", + $ne: 'Mentor', }, }, }, { $lookup: { - from: "timeEntries", - localField: "_id", - foreignField: "personId", - as: "timeEntryData", + from: 'timeEntries', + localField: '_id', + foreignField: 'personId', + as: 'timeEntryData', }, }, { $project: { - personId: "$_id", + personId: '$_id', name: 1, weeklycommittedHours: 1, role: 1, + endDate: 1, timeEntryData: { $filter: { - input: "$timeEntryData", - as: "timeentry", + input: '$timeEntryData', + as: 'timeentry', cond: { $and: [ { - $gte: ["$$timeentry.dateOfWork", pdtstart], + $gte: ['$$timeentry.dateOfWork', pdtstart], }, { - $lte: ["$$timeentry.dateOfWork", pdtend], + $lte: ['$$timeentry.dateOfWork', pdtend], }, { $not: [ { - $in: [ - "$$timeentry.entryType", - ["person", "team", "project"], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, @@ -90,7 +77,7 @@ const dashboardhelper = function () { }, { $unwind: { - path: "$timeEntryData", + path: '$timeEntryData', preserveNullAndEmptyArrays: true, }, }, @@ -98,30 +85,31 @@ const dashboardhelper = function () { $project: { personId: 1, weeklycommittedHours: 1, + endDate: 1, totalSeconds: { $cond: [ { - $gte: ["$timeEntryData.totalSeconds", 0], + $gte: ['$timeEntryData.totalSeconds', 0], }, - "$timeEntryData.totalSeconds", + '$timeEntryData.totalSeconds', 0, ], }, tangibletime: { $cond: [ { - $eq: ["$timeEntryData.isTangible", true], + $eq: ['$timeEntryData.isTangible', true], }, - "$timeEntryData.totalSeconds", + '$timeEntryData.totalSeconds', 0, ], }, intangibletime: { $cond: [ { - $eq: ["$timeEntryData.isTangible", false], + $eq: ['$timeEntryData.isTangible', false], }, - "$timeEntryData.totalSeconds", + '$timeEntryData.totalSeconds', 0, ], }, @@ -130,17 +118,17 @@ const dashboardhelper = function () { { $group: { _id: { - personId: "$personId", - weeklycommittedHours: "$weeklycommittedHours", + personId: '$personId', + weeklycommittedHours: '$weeklycommittedHours', }, time_hrs: { - $sum: { $divide: ["$totalSeconds", 3600] }, + $sum: { $divide: ['$totalSeconds', 3600] }, }, tangibletime_hrs: { - $sum: { $divide: ["$tangibletime", 3600] }, + $sum: { $divide: ['$tangibletime', 3600] }, }, intangibletime_hrs: { - $sum: { $divide: ["$intangibletime", 3600] }, + $sum: { $divide: ['$intangibletime', 3600] }, }, }, }, @@ -148,15 +136,15 @@ const dashboardhelper = function () { $group: { _id: 0, memberCount: { $sum: 1 }, - totalweeklycommittedHours: { $sum: "$_id.weeklycommittedHours" }, + totalweeklycommittedHours: { $sum: '$_id.weeklycommittedHours' }, totaltime_hrs: { - $sum: "$time_hrs", + $sum: '$time_hrs', }, totaltangibletime_hrs: { - $sum: "$tangibletime_hrs", + $sum: '$tangibletime_hrs', }, totalintangibletime_hrs: { - $sum: "$intangibletime_hrs", + $sum: '$intangibletime_hrs', }, }, }, @@ -168,39 +156,39 @@ const dashboardhelper = function () { const getLeaderboard = async function (userId) { const userid = mongoose.Types.ObjectId(userId); try { - const userById = await userProfile.findOne( - { _id: userid, isActive: true }, - { role: 1 } - ); + const userById = await userProfile.findOne({ _id: userid, isActive: true }, { role: 1 }); if (userById == null) return null; const userRole = userById.role; - const pdtstart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .format("YYYY-MM-DD"); + const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment() - .tz("America/Los_Angeles") - .endOf("week") - .format("YYYY-MM-DD"); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); let teamMemberIds = [userid]; let teamMembers = []; - const userAsRequestor = {'role': userRole, requestorId: userId }; + const userAsRequestor = { role: userRole, requestorId: userId }; const canSeeUsersInDashboard = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); if (!canSeeUsersInDashboard) { // Manager , Mentor , Volunteer ... , Show only team members const teamsResult = await team.find( - { "members.userId": { $in: [userid] } }, - { members: 1 } + { 'members.userId': { $in: [userid] } }, + { members: 1 }, ); + console.log(teamsResult); teamsResult.forEach((_myTeam) => { + let isUserVisible = false; _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); + if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; + }); + if(isUserVisible) + { + _myTeam.members.forEach((teamMember) => { + if (!teamMember.userId.equals(userid)) + teamMemberIds.push(teamMember.userId); }); + } + }); teamMembers = await userProfile.find( @@ -214,7 +202,9 @@ const dashboardhelper = function () { weeklySummaries: 1, timeOffFrom: 1, timeOffTill: 1, + endDate: 1, } + ); } else { // 'Core Team', 'Owner' //All users @@ -229,11 +219,13 @@ const dashboardhelper = function () { weeklySummaries: 1, timeOffFrom: 1, timeOffTill: 1, - } + endDate: 1, + + }, ); } - teamMemberIds = teamMembers.map(member => member._id); + teamMemberIds = teamMembers.map((member) => member._id); const timeEntries = await timeentry.find({ dateOfWork: { @@ -241,6 +233,7 @@ const dashboardhelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, + isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -256,11 +249,9 @@ const dashboardhelper = function () { } if (timeEntry.isTangible === true) { - timeEntryByPerson[personIdStr].tangibleSeconds += - timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; } else { - timeEntryByPerson[personIdStr].intangibleSeconds += - timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].intangibleSeconds += timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; @@ -275,28 +266,26 @@ const dashboardhelper = function () { isVisible: teamMember.isVisible, hasSummary: teamMember.weeklySummaries?.length > 0 - ? teamMember.weeklySummaries[0].summary !== "" + ? teamMember.weeklySummaries[0].summary !== '' : false, weeklycommittedHours: teamMember.weeklycommittedHours, totaltangibletime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / - 3600 || 0, + (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds ?? 0) / 3600, totalintangibletime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.intangibleSeconds / - 3600 || 0, - totaltime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || - 0, + (timeEntryByPerson[teamMember._id.toString()]?.intangibleSeconds ?? 0) / 3600, + totaltime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds ?? 0) / 3600, + percentagespentintangible: timeEntryByPerson[teamMember._id.toString()] && timeEntryByPerson[teamMember._id.toString()]?.totalSeconds !== 0 && timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds !== 0 - ? (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / - timeEntryByPerson[teamMember._id.toString()]?.totalSeconds) * + ? ((timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds || 0) / + (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds || 1)) * 100 : 0, timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, + endDate: teamMember.endDate || null, }; leaderBoardData.push(obj); }); @@ -578,15 +567,9 @@ const dashboardhelper = function () { */ const getUserLaborData = async function (userId) { try { - const pdtStart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .format("YYYY-MM-DD"); + const pdtStart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtEnd = moment() - .tz("America/Los_Angeles") - .endOf("week") - .format("YYYY-MM-DD"); + const pdtEnd = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); const user = await userProfile.findById({ _id: userId, @@ -597,7 +580,8 @@ const dashboardhelper = function () { $gte: pdtStart, $lte: pdtEnd, }, - entryType: { $in: ["default", null] }, + entryType: { $in: ['default', null] }, + isActive: { $ne: false }, personId: userId, }); @@ -617,23 +601,23 @@ const dashboardhelper = function () { personId: userId, role: user.role, isVisible: user.isVisible, - hasSummary: user.weeklySummaries[0].summary !== "", + hasSummary: user.weeklySummaries[0].summary !== '', weeklycommittedHours: user.weeklycommittedHours, name: `${user.firstName} ${user.lastName}`, totaltime_hrs: (tangibleSeconds + intangibleSeconds) / 3600, totaltangibletime_hrs: tangibleSeconds / 3600, totalintangibletime_hrs: intangibleSeconds / 3600, - percentagespentintangible: - (intangibleSeconds / tangibleSeconds) * 100, + percentagespentintangible: (intangibleSeconds / tangibleSeconds) * 100, timeOffFrom: user.timeOffFrom, timeOffTill: user.timeOffTill, + endDate: user.endDate || null, }, ]; } catch (err) { return [ { - personId: "error", - name: "Error Error", + personId: 'error', + name: 'Error Error', totaltime_hrs: 0, totaltangibletime_hrs: 0, totalintangibletime_hrs: 0, @@ -644,8 +628,8 @@ const dashboardhelper = function () { }; const laborthismonth = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format("YYYY-MM-DD"); - const todate = moment(endDate).format("YYYY-MM-DD"); + const fromdate = moment(startDate).format('YYYY-MM-DD'); + const todate = moment(endDate).format('YYYY-MM-DD'); return timeentry.aggregate([ { @@ -661,19 +645,19 @@ const dashboardhelper = function () { { $group: { _id: { - projectId: "$projectId", + projectId: '$projectId', }, labor: { - $sum: "$totalSeconds", + $sum: '$totalSeconds', }, }, }, { $lookup: { - from: "projects", - localField: "_id.projectId", - foreignField: "_id", - as: "project", + from: 'projects', + localField: '_id.projectId', + foreignField: '_id', + as: 'project', }, }, { @@ -682,13 +666,13 @@ const dashboardhelper = function () { projectName: { $ifNull: [ { - $arrayElemAt: ["$project.projectName", 0], + $arrayElemAt: ['$project.projectName', 0], }, - "Undefined", + 'Undefined', ], }, timeSpent_hrs: { - $divide: ["$labor", 3600], + $divide: ['$labor', 3600], }, }, }, @@ -696,8 +680,8 @@ const dashboardhelper = function () { }; const laborthisweek = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format("YYYY-MM-DD"); - const todate = moment(endDate).format("YYYY-MM-DD"); + const fromdate = moment(startDate).format('YYYY-MM-DD'); + const todate = moment(endDate).format('YYYY-MM-DD'); return userProfile.aggregate([ { @@ -713,10 +697,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: "timeEntries", - localField: "_id", - foreignField: "personId", - as: "timeEntryData", + from: 'timeEntries', + localField: '_id', + foreignField: 'personId', + as: 'timeEntryData', }, }, { @@ -724,26 +708,23 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: "$timeEntryData", - as: "timeentry", + input: '$timeEntryData', + as: 'timeentry', cond: { $and: [ { - $eq: ["$$timeentry.isTangible", true], + $eq: ['$$timeentry.isTangible', true], }, { - $gte: ["$$timeentry.dateOfWork", fromdate], + $gte: ['$$timeentry.dateOfWork', fromdate], }, { - $lte: ["$$timeentry.dateOfWork", todate], + $lte: ['$$timeentry.dateOfWork', todate], }, { $not: [ { - $in: [ - "$$timeentry.entryType", - ["person", "team", "project"], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, @@ -755,27 +736,27 @@ const dashboardhelper = function () { }, { $unwind: { - path: "$timeEntryData", + path: '$timeEntryData', preserveNullAndEmptyArrays: true, }, }, { $group: { _id: { - _id: "$_id", - weeklycommittedHours: "$weeklycommittedHours", + _id: '$_id', + weeklycommittedHours: '$weeklycommittedHours', }, effort: { - $sum: "$timeEntryData.totalSeconds", + $sum: '$timeEntryData.totalSeconds', }, }, }, { $project: { _id: 0, - weeklycommittedHours: "$_id.weeklycommittedHours", + weeklycommittedHours: '$_id.weeklycommittedHours', timeSpent_hrs: { - $divide: ["$effort", 3600], + $divide: ['$effort', 3600], }, }, }, @@ -783,8 +764,8 @@ const dashboardhelper = function () { }; const laborThisWeekByCategory = function (userId, startDate, endDate) { - const fromdate = moment(startDate).format("YYYY-MM-DD"); - const todate = moment(endDate).format("YYYY-MM-DD"); + const fromdate = moment(startDate).format('YYYY-MM-DD'); + const todate = moment(endDate).format('YYYY-MM-DD'); return userProfile.aggregate([ { @@ -800,10 +781,10 @@ const dashboardhelper = function () { }, { $lookup: { - from: "timeEntries", - localField: "_id", - foreignField: "personId", - as: "timeEntryData", + from: 'timeEntries', + localField: '_id', + foreignField: 'personId', + as: 'timeEntryData', }, }, { @@ -811,26 +792,23 @@ const dashboardhelper = function () { weeklycommittedHours: 1, timeEntryData: { $filter: { - input: "$timeEntryData", - as: "timeentry", + input: '$timeEntryData', + as: 'timeentry', cond: { $and: [ { - $eq: ["$$timeentry.isTangible", true], + $eq: ['$$timeentry.isTangible', true], }, { - $gte: ["$$timeentry.dateOfWork", fromdate], + $gte: ['$$timeentry.dateOfWork', fromdate], }, { - $lte: ["$$timeentry.dateOfWork", todate], + $lte: ['$$timeentry.dateOfWork', todate], }, { $not: [ { - $in: [ - "$$timeentry.entryType", - ["person", "team", "project"], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, @@ -842,37 +820,37 @@ const dashboardhelper = function () { }, { $unwind: { - path: "$timeEntryData", + path: '$timeEntryData', preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: "$timeEntryData.projectId", + _id: '$timeEntryData.projectId', effort: { - $sum: "$timeEntryData.totalSeconds", + $sum: '$timeEntryData.totalSeconds', }, }, }, { $lookup: { - from: "projects", - localField: "_id", - foreignField: "_id", - as: "project", + from: 'projects', + localField: '_id', + foreignField: '_id', + as: 'project', }, }, { $unwind: { - path: "$project", + path: '$project', preserveNullAndEmptyArrays: true, }, }, { $group: { - _id: "$project.category", + _id: '$project.category', effort: { - $sum: "$effort", + $sum: '$effort', }, }, }, @@ -880,7 +858,7 @@ const dashboardhelper = function () { $project: { _id: 1, timeSpent_hrs: { - $divide: ["$effort", 3600], + $divide: ['$effort', 3600], }, }, }, diff --git a/src/helpers/helperModels/userProjects.js b/src/helpers/helperModels/userProjects.js deleted file mode 100644 index a2f1f2b5e..000000000 --- a/src/helpers/helperModels/userProjects.js +++ /dev/null @@ -1,17 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const ProjectSchema = new Schema({ - projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'projects' }, - projectName: { type: String }, - category: { type: String }, -}); - -const userProjectSchema = new Schema({ - - _id: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, - projects: [ProjectSchema], -}); - -module.exports = mongoose.model('userProject', userProjectSchema, 'userProjects'); diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js new file mode 100644 index 000000000..52d6a2ad0 --- /dev/null +++ b/src/helpers/overviewReportHelper.js @@ -0,0 +1,645 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable quotes */ +const Team = require('../models/team'); +const UserProfile = require('../models/userProfile'); +const TimeEntries = require('../models/timeentry'); +const Task = require('../models/task'); + +const overviewReportHelper = function () { + /** + * Get map location statistics + * Group and count all volunteers by their lattitude and longitude + */ + async function getMapLocations() { + return UserProfile.aggregate([ + { + $match: { + isActive: true, + 'location.coords.lat': { $ne: null }, + 'location.coords.lng': { $ne: null }, + }, + }, + { + $group: { + _id: { + lat: '$location.coords.lat', + lng: '$location.coords.lng', + }, + count: { $sum: 1 }, + }, + }, + ]); + } + + /** + * Get the total number of active teams + */ + async function getTotalActiveTeamCount() { + return Team.aggregate([ + { + $match: { + isActive: true, + }, + }, + { + $count: 'activeTeams', + }, + ]); + } + + /** + * Get the users celebrating their anniversary between the two input dates. + * @param {*} startDate + * @param {*} endDate + * @returns The number of users celebrating their anniversary between the two input dates. + */ + async function getAnniversaries(startDate, endDate) { + return UserProfile.aggregate([ + { + $addFields: { + createdMonthDay: { $dateToString: { format: '%m-%d', date: '$createdDate' } }, + }, + }, + { + $match: { + createdMonthDay: { + $gte: startDate.substring(5, 10), + $lte: endDate.substring(5, 10), + }, + isActive: true, + }, + }, + { + $project: { + _id: 1, + firstName: 1, + lastName: 1, + }, + }, + ]); + } + + /** + * Get the number of Blue Square infringements between the two input dates. + * @param {*} startDate + * @param {*} endDate + * @returns + */ + async function getBlueSquareStats(startDate, endDate) { + return UserProfile.aggregate([ + { + $unwind: '$infringements', + }, + { + $match: { + 'infringements.date': { + $gte: startDate, + $lte: endDate, + }, + }, + }, + { + $group: { + _id: '$infringements.description', + count: { $sum: 1 }, + }, + }, + ]); + } + + /** + * Get the number of members in team and not in team, with percentage + */ + async function getTeamMembersCount() { + const [data] = await UserProfile.aggregate([ + { + $match: { + isActive: true, + }, + }, + { + $facet: { + totalMembers: [ + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + count: 1, + }, + }, + ], + + inTeam: [ + { + $match: { + teams: { + $exists: true, + $ne: [], + }, + }, + }, + { + $count: 'usersInTeam', + }, + ], + }, + }, + ]); + + return data; + } + + /** aggregates role distribution statistics + * counts total number of volunteers that fall within each of the different roles + */ + async function getRoleDistributionStats() { + const roleStats = UserProfile.aggregate([ + { + $match: { isActive: true }, + }, + { + $group: { + _id: '$role', + count: { $sum: 1 }, + }, + }, + ]); + + return roleStats; + } + + /** + * aggregates the total number of hours worked between the 5 categories + * Food, Energy, Housing, Stewardship, Society, Economics and Other + */ + async function getWorkDistributionStats(startDate, endDate) { + const distributionStats = TimeEntries.aggregate([ + { + $match: { + dateOfWork: { $gte: startDate, $lte: endDate }, + }, + }, + { + $lookup: { + from: 'projects', + localField: 'projectId', + foreignField: '_id', + as: 'project', + }, + }, + { + $unwind: { + path: '$project', + preserveNullAndEmptyArrays: true, + }, + }, + { + $group: { + _id: '$project.category', + aggregatedSeconds: { $sum: '$totalSeconds' }, + }, + }, + { + $project: { + _id: 1, + totalHours: { $divide: ['$aggregatedSeconds', 3600] }, + }, + }, + ]); + + return distributionStats; + } + + async function getTasksStats(startDate, endDate) { + const taskStats = await Task.aggregate([ + { + $match: { + modifiedDatetime: { $gte: startDate, $lte: endDate }, + status: { $in: ['Complete', 'Active'] }, + }, + }, + { + $group: { + _id: '$status', + count: { $sum: 1 }, + }, + }, + ]); + + if (!taskStats.find((x) => x._id === 'Active')) { + taskStats.push({ _id: 'Active', count: 0 }); + } + if (!taskStats.find((x) => x._id === 'Complete')) { + taskStats.push({ _id: 'Complete', count: 0 }); + } + + return taskStats; + } + /** + * Get the volunteer hours stats, it retrieves the number of hours logged by users between the two input dates as well as their weeklycommittedHours. + * @param {*} startDate + * @param {*} endDate + */ + async function getHoursStats(startDate, endDate) { + const hoursStats = await UserProfile.aggregate([ + { + $match: { + isActive: true, + }, + }, + { + $lookup: { + from: 'timeEntries', // The collection to join + localField: '_id', // Field from the userProfile collection + foreignField: 'personId', // Field from the timeEntries collection + as: 'timeEntries', // The array field that will contain the joined documents + }, + }, + { + $unwind: { + path: '$timeEntries', + preserveNullAndEmptyArrays: true, // Preserve users with no time entries + }, + }, + { + $match: { + $or: [ + { timeEntries: { $exists: false } }, + { 'timeEntries.dateOfWork': { $gte: startDate, $lte: endDate } }, + ], + }, + }, + { + $group: { + _id: '$_id', + personId: { $first: '$_id' }, + totalSeconds: { $sum: '$timeEntries.totalSeconds' }, // Sum seconds from timeEntries + weeklycommittedHours: { $first: `$weeklycommittedHours` }, // Include the weeklycommittedHours field + }, + }, + { + $project: { + totalHours: { $divide: ['$totalSeconds', 3600] }, // Convert seconds to hours + weeklycommittedHours: 1, // make sure we include it in the end result + }, + }, + { + $bucket: { + groupBy: '$totalHours', + boundaries: [0, 10, 20, 30, 40], + default: 40, + output: { + count: { $sum: 1 }, + }, + }, + }, + ]); + for (let i = 0; i < 5; i++) { + if (!hoursStats.find((x) => x._id === i * 10)) { + hoursStats.push({ _id: i * 10, count: 0 }); + } + } + return hoursStats; + } + + /** + * Aggregates total number of hours worked across all volunteers within the specified date range + */ + async function getTotalHoursWorked(startDate, endDate) { + console.log(startDate, endDate); + const data = await TimeEntries.aggregate([ + { + $match: { + dateOfWork: { $gte: startDate, $lte: endDate }, + }, + }, + { + $group: { + _id: null, + totalSeconds: { $sum: '$totalSeconds' }, + }, + }, + { + $project: { + _id: 0, + totalHours: { $divide: ['$totalSeconds', 3600] }, + }, + }, + ]); + + return data; + } + + /** + * returns the number of: + * 1. Active volunteers + * 2. Volunteers that deactivated in the current week + * 3. New volunteers in the current week + * + * @param {string} startDate + * @param {string} endDate + */ + const getVolunteerNumberStats = async (startDate, endDate) => { + const [data] = await UserProfile.aggregate([ + { + $facet: { + activeVolunteers: [{ $match: { isActive: true } }, { $count: 'activeVolunteersCount' }], + + newVolunteers: [ + { + $match: { + createdDate: { + $gte: startDate, + $lte: endDate, + }, + }, + }, + { $count: 'newVolunteersCount' }, + ], + + deactivatedVolunteers: [ + { + $match: { + $and: [ + { lastModifiedDate: { $gte: startDate } }, + { lastModifiedDate: { $lte: endDate } }, + { isActive: false }, + ], + }, + }, + { $count: 'deactivedVolunteersCount' }, + ], + }, + }, + ]); + + return data; + }; + + /** + * + * @returns The number of teams with 4 or more members. + */ + async function getFourPlusMembersTeamCount() { + // check if members array has 4 or more members + return Team.countDocuments({ 'members.4': { $exists: true } }); + } + + /** + * Get the total number of badges awarded between the two input dates. + * @param {*} startDate + * @param {*} endDate + * @returns The total number of badges awarded between the two input dates. + */ + async function getTotalBadgesAwardedCount(startDate, endDate) { + return UserProfile.aggregate([ + { + $unwind: '$badgeCollection', + }, + { + $match: { + 'badgeCollection.earnedDate': { + $gte: startDate, + $lte: endDate, + }, + }, + }, + { + $count: 'badgeCollection', + }, + ]); + } + + /** + * Get the number of users celebrating their anniversary between the two input dates. + * @param {*} startDate + * @param {*} endDate + * @returns The number of users celebrating their anniversary between the two input dates. + */ + async function getAnniversaryCount(startDate, endDate) { + return UserProfile.aggregate([ + { + $addFields: { + createdMonthDay: { $dateToString: { format: '%m-%d', date: '$createdDate' } }, + }, + }, + { + $match: { + createdMonthDay: { + $gte: new Date(startDate).toISOString().substring(5, 10), + $lte: new Date(endDate).toISOString().substring(5, 10), + }, + }, + }, + { + $count: 'anniversaryCount', + }, + ]); + } + + /** + * Get the role and count of users. + * @returns The role and count of users. + */ + async function getRoleCount() { + return UserProfile.aggregate([ + { + $group: { + _id: '$role', + count: { $sum: 1 }, + }, + }, + ]); + } + + /** + * Get the number of active and inactive users. + */ + async function getActiveInactiveUsersCount() { + const activeUsers = await UserProfile.countDocuments({ isActive: true }); + const inactiveUsers = await UserProfile.countDocuments({ isActive: false }); + + return { + activeUsers, + inactiveUsers, + }; + } + + /** + * Groups users based off of hours logged and the percentage of hours logged divided by their weeklycommittedHours for the current week and last week. + * @param {*} startDate + * @param {*} endDate + */ + async function getVolunteerHoursStats(startDate, endDate, lastWeekStartDate, lastWeekEndDate) { + const currentWeekStats = await getHoursStats(startDate, endDate); + const lastWeekStats = await getHoursStats(lastWeekStartDate, lastWeekEndDate); + + const volunteerHoursStats = { + numberOfUsers: currentWeekStats.length, + }; + + // + const percentageWorkedStats = { + thisWeek: { '<100': 0, '100-109': 0, '110-149': 0, '150-199': 0, '200+': 0 }, + lastWeek: { '<100': 0, '100-109': 0, '110-149': 0, '150-199': 0, '200+': 0 }, + }; + + for (let i = 0; i < 6; i++) { + const group = i * 10; + volunteerHoursStats[`${group}-${group + 9}`] = 0; + } + volunteerHoursStats['60+'] = 0; + + // Group users by the number of hours logged as well as percentage of weeklycommittedHours worked + currentWeekStats.forEach((user) => { + if (user.totalHours >= 60) { + volunteerHoursStats['60+'] = volunteerHoursStats['60+'] + ? volunteerHoursStats['60+'] + 1 + : 1; + console.log('user with 60+ hours'); + } else { + const group = Math.floor(user.totalHours / 10) * 10; + volunteerHoursStats[`${group}-${group + 9}`] += 1; + } + + const percentage = user.totalHours / user.weeklycommittedHours; + + if (percentage < 1) { + percentageWorkedStats.thisWeek['<100'] += 1; + } else if (percentage < 1.1) { + percentageWorkedStats.thisWeek['100-109'] += 1; + } else if (percentage < 1.5) { + percentageWorkedStats.thisWeek['110-149'] += 1; + } else if (percentage < 2) { + percentageWorkedStats.thisWeek['150-199'] += 1; + } else { + percentageWorkedStats.thisWeek['200+'] += 1; + } + }); + + // now we need to group last weeks statistics by percentage of weeklycommittedHours worked + lastWeekStats.forEach((user) => { + const percentage = user.totalHours / user.weeklycommittedHours; + if (percentage < 1) { + percentageWorkedStats.lastWeek['<100'] += 1; + } else if (percentage < 1.1) { + percentageWorkedStats.lastWeek['100-109'] += 1; + } else if (percentage < 1.5) { + percentageWorkedStats.lastWeek['110-149'] += 1; + } else if (percentage < 2) { + percentageWorkedStats.lastWeek['150-199'] += 1; + } else { + percentageWorkedStats.lastWeek['200+'] += 1; + } + }); + + return { volunteerHoursStats, percentageWorkedStats }; + } + + /** + * 1. Total hours logged in tasks + * 2. Total hours logged in projects + * 3. Number of member with tasks assigned + * 4. Number of member without tasks assigned + * 5. Number of tasks with due date within the date range + * @param {*} startDate + * @param {*} endDate + */ + async function getTaskAndProjectStats(startDate, endDate) { + // 1. Total hours logged in tasks + const taskHours = await TimeEntries.aggregate([ + { + $match: { + dateOfWork: { $gte: startDate, $lte: endDate }, + taskId: { $exists: true }, + }, + }, + { + $group: { + _id: null, + totalSeconds: { $sum: '$totalSeconds' }, + }, + }, + { + $project: { + totalHours: { $divide: ['$totalSeconds', 3600] }, + }, + }, + ]); + + // 2. Total hours logged in projects + const projectHours = await TimeEntries.aggregate([ + { + $match: { + dateOfWork: { $gte: startDate, $lte: endDate }, + projectId: { $exists: true }, + }, + }, + { + $group: { + _id: null, + totalSeconds: { $sum: '$totalSeconds' }, + }, + }, + { + $project: { + totalHours: { $divide: ['$totalSeconds', 3600] }, + }, + }, + ]); + + // 3. Number of member with tasks assigned + const membersWithTasks = await Task.distinct('resources.userID', { + 'resources.userID': { $exists: true }, + completedTask: { $ne: true }, + }); + + // 4. Number of member without tasks assigned + const membersWithoutTasks = await UserProfile.countDocuments({ + _id: { $nin: membersWithTasks }, + }); + + // 5. Number of tasks with due date within the date range + const tasksDueWithinDate = await Task.countDocuments({ + dueDatetime: { $gte: startDate, $lte: endDate }, + }); + + const taskAndProjectStats = { + taskHours: taskHours[0].totalHours.toFixed(2), + projectHours: projectHours[0].totalHours.toFixed(2), + membersWithTasks: membersWithTasks.length, + membersWithoutTasks, + tasksDueThisWeek: tasksDueWithinDate, + }; + + return taskAndProjectStats; + } + + return { + getMapLocations, + getTotalActiveTeamCount, + getAnniversaries, + getRoleDistributionStats, + getVolunteerNumberStats, + getTasksStats, + getWorkDistributionStats, + getTotalHoursWorked, + getHoursStats, + getFourPlusMembersTeamCount, + getTotalBadgesAwardedCount, + getAnniversaryCount, + getRoleCount, + getBlueSquareStats, + getTeamMembersCount, + getActiveInactiveUsersCount, + getVolunteerHoursStats, + getTaskAndProjectStats, + }; +}; + +module.exports = overviewReportHelper; diff --git a/src/helpers/overviewReportHelper.spec.js b/src/helpers/overviewReportHelper.spec.js new file mode 100644 index 000000000..44fb7bf83 --- /dev/null +++ b/src/helpers/overviewReportHelper.spec.js @@ -0,0 +1,64 @@ +const overviewReportHelper = require('./overviewReportHelper'); +const UserProfile = require('../models/userProfile'); + +const makeSut = () => { + const { getVolunteerNumberStats } = overviewReportHelper(); + + return { getVolunteerNumberStats }; +}; + +describe('overviewReportHelper method tests', () => { + const startDate = '2024-05-26T00:00:00Z'; + const endDate = '2024-06-02T00:00:00Z'; + + describe('getVolunteerNumberStats method', () => { + test('it should call the aggregation method on UserProfile', async () => { + const { getVolunteerNumberStats } = makeSut(); + const aggregateSpy = jest.spyOn(UserProfile, 'aggregate').mockImplementationOnce(() => null); + + await getVolunteerNumberStats(startDate, endDate); + + expect(aggregateSpy).toHaveBeenCalled(); + }); + + test('it should call the aggregation query with the correct parameters', async () => { + const { getVolunteerNumberStats } = makeSut(); + const aggregateSpy = jest.spyOn(UserProfile, 'aggregate').mockImplementationOnce(() => null); + + await getVolunteerNumberStats(startDate, endDate); + + expect(aggregateSpy).toHaveBeenCalled(); + expect(aggregateSpy).toHaveBeenCalledWith([ + { + $facet: { + activeVolunteers: [{ $match: { isActive: true } }, { $count: 'activeVolunteersCount' }], + + newVolunteers: [ + { + $match: { + createdDate: { + $gte: startDate, + $lte: endDate, + }, + }, + }, + { $count: 'newVolunteersCount' }, + ], + + deactivatedVolunteers: [ + { + $match: { + $and: [ + { lastModifiedDate: { $gte: startDate } }, + { lastModifiedDate: { $lte: endDate } }, + { isActive: false }, + ], + }, + }, + ], + }, + }, + ]); + }); + }); +}); diff --git a/src/helpers/taskHelper.js b/src/helpers/taskHelper.js index dca64cb66..34fb36be8 100644 --- a/src/helpers/taskHelper.js +++ b/src/helpers/taskHelper.js @@ -11,7 +11,6 @@ const taskHelper = function () { const getTasksForTeams = async function (userId, requestor) { const userid = mongoose.Types.ObjectId(userId); const requestorId = mongoose.Types.ObjectId(requestor.requestorId); - const requestorRole = requestor.role; try { const userById = await userProfile.findOne( { _id: userid, isActive: true }, @@ -22,103 +21,128 @@ const taskHelper = function () { isVisible: 1, weeklycommittedHours: 1, weeklySummaries: 1, + weeklySummaryOption: 1, timeOffFrom: 1, timeOffTill: 1, + teamCode: 1, + teams: 1, adminLinks: 1, - } + }, ); if (userById === null) return null; const userRole = userById.role; - const pdtstart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .format("YYYY-MM-DD"); - const pdtend = moment() - .tz("America/Los_Angeles") - .endOf("week") - .format("YYYY-MM-DD"); + const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); let teamMemberIds = [userid]; let teamMembers = []; const isRequestorOwnerLike = await hasPermission(requestor, 'seeUsersInDashboard'); - const userAsRequestor = {'role': userRole, requestorId: userId }; + const userAsRequestor = { role: userRole, requestorId: userId }; const isUserOwnerLike = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); switch (true) { case isRequestorOwnerLike && isUserOwnerLike: { - teamMembers = await userProfile.find( - { isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - } - ); + teamMembers = await userProfile + .find( + { isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); break; } case isRequestorOwnerLike && !isUserOwnerLike: { const teamsResult = await team.find( - { "members.userId": { $in: [userid] } }, - { members: 1 } + { 'members.userId': { $in: [userid] } }, + { members: 1 }, ); teamsResult.forEach((_myTeam) => { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); + if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); }); }); - teamMembers = await userProfile.find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - } - ); + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); break; } default: { const sharedTeamsResult = await team.find( - { "members.userId": { $all: [userid, requestorId] } }, - { members: 1 } + { 'members.userId': { $all: [userid, requestorId] } }, + { members: 1 }, ); sharedTeamsResult.forEach((_myTeam) => { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); + if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); }); }); - teamMembers = await userProfile.find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - } - ); + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); } } - teamMemberIds = teamMembers.map(member => member._id); + teamMemberIds = teamMembers.map((member) => member._id); const timeEntries = await timeentry.find({ dateOfWork: { @@ -126,6 +150,7 @@ const taskHelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, + isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -139,19 +164,18 @@ const taskHelper = function () { }; } if (timeEntry.isTangible) { - timeEntryByPerson[personIdStr].tangibleSeconds += - timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; }); const teamMemberTasks = await Task.find( - { "resources.userID": { $in: teamMemberIds } }, - { "resources.profilePic": 0 } + { 'resources.userID': { $in: teamMemberIds } }, + { 'resources.profilePic': 0 }, ).populate({ - path: "wbsId", - select: "projectId", + path: 'wbsId', + select: 'projectId', }); - const teamMemberTaskIds = teamMemberTasks.map(task => task._id); + const teamMemberTaskIds = teamMemberTasks.map((task) => task._id); const teamMemberTaskNotifications = await TaskNotification.find({ taskId: { $in: teamMemberTaskIds }, }); @@ -163,13 +187,9 @@ const taskHelper = function () { const taskNdUserID = `${taskIdStr},${userIdStr}`; if (taskNotificationByTaskNdUser[taskNdUserID]) { - taskNotificationByTaskNdUser[taskNdUserID].push( - teamMemberTaskNotification - ); + taskNotificationByTaskNdUser[taskNdUserID].push(teamMemberTaskNotification); } else { - taskNotificationByTaskNdUser[taskNdUserID] = [ - teamMemberTaskNotification, - ]; + taskNotificationByTaskNdUser[taskNdUserID] = [teamMemberTaskNotification]; } }); @@ -183,8 +203,11 @@ const taskHelper = function () { teamMemberTask.resources.forEach((resource) => { const resourceIdStr = resource.userID?.toString(); const taskNdUserID = `${taskIdStr},${resourceIdStr}`; - _teamMemberTask.taskNotifications = - taskNotificationByTaskNdUser[taskNdUserID] || []; + // initialize taskNotifications if not exists + if (!_teamMemberTask.taskNotifications) _teamMemberTask.taskNotifications = []; + // push all notifications into the list if taskNdUserId key exists + if (taskNotificationByTaskNdUser[taskNdUserID]) + _teamMemberTask.taskNotifications.push(...taskNotificationByTaskNdUser[taskNdUserID]); if (taskByPerson[resourceIdStr]) { taskByPerson[resourceIdStr].push(_teamMemberTask); } else { @@ -195,20 +218,22 @@ const taskHelper = function () { const teamMemberTasksData = []; teamMembers.forEach((teamMember) => { + const timeEntry = timeEntryByPerson[teamMember._id.toString()]; + const tangible = timeEntry?.tangibleSeconds || 0; + const total = timeEntry?.totalSeconds || 0; const obj = { personId: teamMember._id, role: teamMember.role, name: `${teamMember.firstName} ${teamMember.lastName}`, weeklycommittedHours: teamMember.weeklycommittedHours, - totaltangibletime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds / - 3600 || 0, - totaltime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 || - 0, + weeklySummaryOption: teamMember.weeklySummaryOption || null, + totaltangibletime_hrs: tangible / 3600, + totaltime_hrs: total / 3600, tasks: taskByPerson[teamMember._id.toString()] || [], timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, + teamCode: teamMember.teamCode || null, + teams: teamMember.teams || null, adminLinks: teamMember.adminLinks || null, }; teamMemberTasksData.push(obj); @@ -504,14 +529,8 @@ const taskHelper = function () { // ]); }; const getTasksForSingleUser = function (userId) { - const pdtstart = moment() - .tz("America/Los_Angeles") - .startOf("week") - .format("YYYY-MM-DD"); - const pdtend = moment() - .tz("America/Los_Angeles") - .endOf("week") - .format("YYYY-MM-DD"); + const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); return userProfile.aggregate([ { $match: { @@ -520,33 +539,33 @@ const taskHelper = function () { }, { $project: { - personId: "$_id", - role: "$role", + personId: '$_id', + role: '$role', name: { - $concat: ["$firstName", " ", "$lastName"], + $concat: ['$firstName', ' ', '$lastName'], }, weeklycommittedHours: { $sum: [ - "$weeklycommittedHours", + '$weeklycommittedHours', { - $ifNull: ["$missedHours", 0], + $ifNull: ['$missedHours', 0], }, ], }, timeOffFrom: { - $ifNull: ["$timeOffFrom", null], + $ifNull: ['$timeOffFrom', null], }, timeOffTill: { - $ifNull: ["$timeOffTill", null], + $ifNull: ['$timeOffTill', null], }, }, }, { $lookup: { - from: "timeEntries", - localField: "personId", - foreignField: "personId", - as: "timeEntryData", + from: 'timeEntries', + localField: 'personId', + foreignField: 'personId', + as: 'timeEntryData', }, }, { @@ -559,18 +578,21 @@ const taskHelper = function () { role: 1, timeEntryData: { $filter: { - input: "$timeEntryData", - as: "timeentry", + input: '$timeEntryData', + as: 'timeentry', cond: { $and: [ { - $gte: ["$$timeentry.dateOfWork", pdtstart], + $gte: ['$$timeentry.dateOfWork', pdtstart], }, { - $lte: ["$$timeentry.dateOfWork", pdtend], + $lte: ['$$timeentry.dateOfWork', pdtend], }, { - $in: ["$$timeentry.entryType", ["default", null]], + $in: ['$$timeentry.entryType', ['default', null]], + }, + { + $ne: ['$$timeentry.isActive', false], }, ], }, @@ -580,7 +602,7 @@ const taskHelper = function () { }, { $unwind: { - path: "$timeEntryData", + path: '$timeEntryData', preserveNullAndEmptyArrays: true, }, }, @@ -595,18 +617,18 @@ const taskHelper = function () { totalSeconds: { $cond: [ { - $gte: ["$timeEntryData.totalSeconds", 0], + $gte: ['$timeEntryData.totalSeconds', 0], }, - "$timeEntryData.totalSeconds", + '$timeEntryData.totalSeconds', 0, ], }, isTangible: { $cond: [ { - $gte: ["$timeEntryData.totalSeconds", 0], + $gte: ['$timeEntryData.totalSeconds', 0], }, - "$timeEntryData.isTangible", + '$timeEntryData.isTangible', false, ], }, @@ -617,9 +639,9 @@ const taskHelper = function () { tangibletime: { $cond: [ { - $eq: ["$isTangible", true], + $eq: ['$isTangible', true], }, - "$totalSeconds", + '$totalSeconds', 0, ], }, @@ -628,76 +650,81 @@ const taskHelper = function () { { $group: { _id: { - personId: "$personId", - weeklycommittedHours: "$weeklycommittedHours", - timeOffFrom: "$timeOffFrom", - timeOffTill: "$timeOffTill", - name: "$name", - role: "$role", + personId: '$personId', + weeklycommittedHours: '$weeklycommittedHours', + timeOffFrom: '$timeOffFrom', + timeOffTill: '$timeOffTill', + name: '$name', + role: '$role', }, totalSeconds: { - $sum: "$totalSeconds", + $sum: '$totalSeconds', }, tangibletime: { - $sum: "$tangibletime", + $sum: '$tangibletime', }, }, }, { $project: { _id: 0, - personId: "$_id.personId", - name: "$_id.name", - weeklycommittedHours: "$_id.weeklycommittedHours", - timeOffFrom: "$_id.timeOffFrom", - timeOffTill: "$_id.timeOffTill", - role: "$_id.role", + personId: '$_id.personId', + name: '$_id.name', + weeklycommittedHours: '$_id.weeklycommittedHours', + timeOffFrom: '$_id.timeOffFrom', + timeOffTill: '$_id.timeOffTill', + role: '$_id.role', totaltime_hrs: { - $divide: ["$totalSeconds", 3600], + $divide: ['$totalSeconds', 3600], }, totaltangibletime_hrs: { - $divide: ["$tangibletime", 3600], + $divide: ['$tangibletime', 3600], }, }, }, { $lookup: { - from: "tasks", - localField: "personId", - foreignField: "resources.userID", - as: "tasks", + from: 'tasks', + localField: 'personId', + foreignField: 'resources.userID', + as: 'tasks', }, }, { $project: { tasks: { - resources: { - profilePic: 0, + $filter: { + input: '$tasks', + as: 'task', + cond: { + $ne: ['$$task.isActive', false], + }, }, }, + 'tasks.resources.profilePic': 0, }, }, { $unwind: { - path: "$tasks", + path: '$tasks', preserveNullAndEmptyArrays: true, }, }, { $lookup: { - from: "wbs", - localField: "tasks.wbsId", - foreignField: "_id", - as: "projectId", + from: 'wbs', + localField: 'tasks.wbsId', + foreignField: '_id', + as: 'projectId', }, }, { $addFields: { - "tasks.projectId": { + 'tasks.projectId': { $cond: [ - { $ne: ["$projectId", []] }, - { $arrayElemAt: ["$projectId", 0] }, - "$tasks.projectId", + { $ne: ['$projectId', []] }, + { $arrayElemAt: ['$projectId', 0] }, + '$tasks.projectId', ], }, }, @@ -719,40 +746,40 @@ const taskHelper = function () { }, { $addFields: { - "tasks.projectId": "$tasks.projectId.projectId", + 'tasks.projectId': '$tasks.projectId.projectId', }, }, { $lookup: { - from: "taskNotifications", - localField: "tasks._id", - foreignField: "taskId", - as: "tasks.taskNotifications", + from: 'taskNotifications', + localField: 'tasks._id', + foreignField: 'taskId', + as: 'tasks.taskNotifications', }, }, { $group: { - _id: "$personId", - tasks: { $push: "$tasks" }, + _id: '$personId', + tasks: { $push: '$tasks' }, data: { - $first: "$$ROOT", + $first: '$$ROOT', }, }, }, { $addFields: { - "data.tasks": { + 'data.tasks': { $filter: { - input: "$tasks", - as: "task", - cond: { $ne: ["$$task", {}] }, + input: '$tasks', + as: 'task', + cond: { $ne: ['$$task', {}] }, }, }, }, }, { $replaceRoot: { - newRoot: "$data", + newRoot: '$data', }, }, ]); @@ -760,7 +787,7 @@ const taskHelper = function () { const getUserProfileFirstAndLastName = function (userId) { return userProfile.findById(userId).then((results) => { if (!results) { - return " "; + return ' '; } return `${results.firstName} ${results.lastName}`; }); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 634e7de99..4fbe3376e 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -22,6 +22,7 @@ const reportHelper = require('./reporthelper')(); const emailSender = require('../utilities/emailSender'); const logger = require('../startup/logger'); const token = require('../models/profileInitialSetupToken'); +const BlueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); const cache = require('../utilities/nodeCache')(); const timeOffRequest = require('../models/timeOffRequest'); const notificationService = require('../services/notificationService'); @@ -46,6 +47,24 @@ const userHelper = function () { }); }; + const getTeamManagementEmail = function (teamId) { + const parsedTeamId = mongoose.Types.ObjectId(teamId); + return userProfile + .find( + { + isActive: true, + teams: { + $in: [parsedTeamId], + }, + role: { + $in: ['Manager', 'Administrator'], + }, + }, + 'email role', + ) + .exec(); + }; + const getUserName = async function (userId) { const userid = mongoose.Types.ObjectId(userId); return userProfile.findById(userid, 'firstName lastName'); @@ -106,26 +125,70 @@ const userHelper = function () { coreTeamExtraHour, requestForTimeOffEmailBody, administrativeContent, + weeklycommittedHours, ) { let finalParagraph = ''; - + let descrInfringement = ''; if (timeRemaining === undefined) { finalParagraph = '

      Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.

      '; + descrInfringement = `

      Total Infringements: This is your ${moment + .localeData() + .ordinal(totalInfringements)} blue square of 5.

      `; } else { + let hrThisweek = weeklycommittedHours || 0 + coreTeamExtraHour; + const remainHr = timeRemaining || 0; + hrThisweek += remainHr; finalParagraph = `Please complete ALL owed time this week (${ - timeRemaining + coreTeamExtraHour + hrThisweek + totalInfringements - 5 } hours) to avoid receiving another blue square. If you have any questions about any of this, please see the "One Community Core Team Policies and Procedures" page.`; + descrInfringement = `

      Total Infringements: This is your ${moment + .localeData() + .ordinal( + totalInfringements, + )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your + requirement this week. This is in addition to any hours missed for last week: + ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours + owed for this being your ${moment + .localeData() + .ordinal( + totalInfringements, + )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. + .

      `; } - // bold description for 'not submitting a weekly summary' and logged hrs + // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs let emailDescription = requestForTimeOffEmailBody; if (!requestForTimeOffEmailBody && infringement.description) { - if (infringement.description.includes('not submitting a weekly summary')) { - emailDescription = infringement.description.replace( - /(not submitting a weekly summary)/gi, + const sentences = infringement.description.split('.'); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + emailDescription = sentences.join('.'); + emailDescription = emailDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes('System auto-assigned infringement for editing your time entries') + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + emailDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace(/(not submitting a weekly summary)/gi, '$1'); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, '$1', ); - emailDescription = emailDescription.replace(/(\d+\.\d{2})\s*hours/i, '$1 hours'); + emailDescription = sentences.join('.'); + emailDescription = emailDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); } else { emailDescription = `${infringement.description}`; } @@ -133,13 +196,12 @@ const userHelper = function () { // add administrative content const text = `Dear ${firstName} ${lastName},

      Oops, it looks like something happened and you’ve managed to get a blue square.

      -

      Date Assigned: ${infringement.date}

      \ +

      Date Assigned: ${moment(infringement.date).format('M-D-YYYY')}

      \

      Description: ${emailDescription}

      -

      Total Infringements: This is your ${moment - .localeData() - .ordinal(totalInfringements)} blue square of 5.

      + ${descrInfringement} ${finalParagraph} -

      Thank you, One Community

      +

      Thank you,

      +

      One Community

             
      @@ -165,7 +227,7 @@ const userHelper = function () { */ const emailWeeklySummariesForAllUsers = async (weekIndex = 1) => { const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - + /* eslint-disable no-undef */ logger.logInfo( `Job for emailing all users' weekly summaries starting at ${currentFormattedDate}`, ); @@ -177,7 +239,7 @@ const userHelper = function () { const results = await reportHelper.weeklySummaries(weekIndex, weekIndex); // checks for userProfiles who are eligible to receive the weeklySummary Reports await userProfile - .find({ getWeeklyReport: true }, { email: 1, _id: 0 }) + .find({ getWeeklyReport: true }, { email: 1, teamCode: 1, _id: 0 }) // eslint-disable-next-line no-shadow .then((results) => { mappedResults = results.map((ele) => ele.email); @@ -209,6 +271,7 @@ const userHelper = function () { weeklySummariesCount, weeklycommittedHours, weeklySummaryOption, + teamCode, } = result; if (email !== undefined && email !== null) { @@ -221,7 +284,7 @@ const userHelper = function () { const hoursLogged = result.totalSeconds[0] / 3600 || 0; const mediaUrlLink = mediaUrl ? `${mediaUrl}` : 'Not provided!'; - + const teamCodeStr = teamCode ? `${teamCode}` : 'X-XXX'; const googleDocLinkValue = adminLinks?.length > 0 ? adminLinks.find((link) => link.Name === 'Google Doc' && link.Link) @@ -271,6 +334,9 @@ const userHelper = function () { \n
      Name: ${firstName} ${lastName} +

      + Team Code: ${teamCodeStr || 'X-XXX'} +

      @@ -370,6 +436,7 @@ const userHelper = function () { */ const assignBlueSquareForTimeNotMet = async () => { try { + console.log('run'); const currentFormattedDate = moment().tz('America/Los_Angeles').format(); moment.tz('America/Los_Angeles').startOf('day').toISOString(); @@ -445,16 +512,22 @@ const userHelper = function () { * Condition: * 1. Not Started: Start Date > end date of last week && totalTangibleHrs === 0 && totalIntangibleHrs === 0 * 2. Short Week: Start Date (First time entrie) is after Monday && totalTangibleHrs === 0 && totalIntangibleHrs === 0 - * 3. No hour logged + * 3. No hours logged, and the account was after the start of last week. * * Notes: - * 1. Start date is automatically updated upon frist time-log. + * 1. Start date is automatically updated upon first time-log. * 2. User meet above condition but meet minimum hours without submitting weekly summary * should get a blue square as reminder. * */ let isNewUser = false; const userStartDate = moment(person.startDate); - if (person.totalTangibleHrs === 0 && person.totalIntangibleHrs === 0 && timeSpent === 0) { + if ( + person.totalTangibleHrs === 0 && + person.totalIntangibleHrs === 0 && + timeSpent === 0 && + userStartDate.isAfter(pdtStartOfLastWeek) + ) { + console.log('1'); isNewUser = true; } @@ -464,6 +537,7 @@ const userHelper = function () { userStartDate.isBefore(pdtEndOfLastWeek) && timeUtils.getDayOfWeekStringFromUTC(person.startDate) > 1) ) { + console.log('2'); isNewUser = true; } @@ -521,33 +595,68 @@ const userHelper = function () { historyInfringements = oldInfringements .map((item, index) => { let enhancedDescription; - if ( - item.description && - !item.description.includes('System auto-assigned infringement') - ) { - enhancedDescription = `${item.description}`; - } else if (item.description) { - // highlight not submitting a weekly summary and logged hrs - const sentences = item.description.split(/\.(?!\d)/); - sentences[0] = `${sentences[0]}`; - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = enhancedDescription.replace( - /(\d+\.\d{2})\s*hours/i, - '$1 hours', + if (item.description) { + let sentences = item.description.split('.'); + const dateRegex = + /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; + sentences = sentences.map((sentence) => + sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { + const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; + }), ); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes( + 'System auto-assigned infringement for editing your time entries', + ) + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + enhancedDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + enhancedDescription = `${item.description}`; + } } - return `

      ${index + 1}. Date: ${item.date}, Description: ${enhancedDescription}

      `; + return `

      ${index + 1}. Date: ${moment( + item.date, + ).format('M-D-YYYY')}, Description: ${enhancedDescription}

      `; }) .join(''); } // No extra hours is needed if blue squares isn't over 5. // length +1 is because new infringement hasn't been created at this stage. - const coreTeamExtraHour = Math.max(0, oldInfringements.length + 1 - 5); - + const coreTeamExtraHour = Math.max(0, oldInfringements.length - 5); const utcStartMoment = moment(pdtStartOfLastWeek).add(1, 'second'); const utcEndMoment = moment(pdtEndOfLastWeek).subtract(1, 'day').subtract(1, 'second'); @@ -568,10 +677,10 @@ const userHelper = function () { // eslint-disable-next-line prefer-destructuring requestForTimeOff = requestsForTimeOff[0]; requestForTimeOffStartingDate = moment(requestForTimeOff.startingDate).format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', ); requestForTimeOffEndingDate = moment(requestForTimeOff.endingDate).format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', ); requestForTimeOffreason = requestForTimeOff.reason; requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; @@ -583,9 +692,9 @@ const userHelper = function () { } else if (timeNotMet && !hasWeeklySummary) { if (person.role === 'Core Team') { description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. In the week starting ${pdtStartOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )} and ending ${pdtEndOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ person.weeklycommittedHours } hours + ${ @@ -601,15 +710,15 @@ const userHelper = function () { description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. For the hours portion, you logged ${timeSpent.toFixed( 2, )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; } } else if (timeNotMet) { if (person.role === 'Core Team') { description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. In the week starting ${pdtStartOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )} and ending ${pdtEndOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ user.weeklycommittedHours } hours + ${ @@ -625,13 +734,13 @@ const userHelper = function () { description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. You logged ${timeSpent.toFixed( 2, )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; } } else { description = `System auto-assigned infringement for not submitting a weekly summary for the week starting ${pdtStartOfLastWeek.format( - 'dddd YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; } const infringement = { @@ -656,7 +765,7 @@ const userHelper = function () { { new: true }, ); const administrativeContent = { - startDate: moment(person.startDate).utc().format('YYYY-MM-DD'), + startDate: moment(person.startDate).utc().format('M-D-YYYY'), role: person.role, userTitle: person.jobTitle[0], historyInfringements, @@ -671,6 +780,7 @@ const userHelper = function () { coreTeamExtraHour, requestForTimeOffEmailBody, administrativeContent, + weeklycommittedHours, ); } else { emailBody = getInfringementEmailBody( @@ -684,11 +794,27 @@ const userHelper = function () { administrativeContent, ); } + + let emailsBCCs; + /* eslint-disable array-callback-return */ + const blueSquareBCCs = await BlueSquareEmailAssignment.find() + .populate('assignedTo') + .exec(); + if (blueSquareBCCs.length > 0) { + emailsBCCs = blueSquareBCCs.map((assignment) => { + if (assignment.assignedTo.isActive === true) { + return assignment.email; + } + }); + } else { + emailsBCCs = null; + } + emailSender( status.email, 'New Infringement Assigned', emailBody, - null, + emailsBCCs, 'onecommunityglobal@gmail.com', status.email, null, @@ -898,7 +1024,8 @@ const userHelper = function () { }, ); - logger.logInfo(results); + logger.logInfo(`Job deleting blue squares older than 1 year finished + at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); } catch (err) { logger.logException(err); } @@ -981,30 +1108,66 @@ const userHelper = function () { if (original.length) { historyInfringements = original .map((item, index) => { - let enhancedDescription = item.description; - // highlight previous assigned reason manually - if (item.description && !item.description.includes('System auto-assigned infringement')) { - enhancedDescription = `${item.description}`; - } else { - // highlight not submitting a weekly summary and logged hrs - const sentences = item.description.split(/\.(?!\d)/); - sentences[0] = `${sentences[0]}`; - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = enhancedDescription.replace( - /(\d+\.\d{2})\s*hours/i, - '$1 hours', + let enhancedDescription; + if (item.description) { + let sentences = item.description.split('.'); + const dateRegex = + /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; + sentences = sentences.map((sentence) => + sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { + const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; + }), ); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes( + 'System auto-assigned infringement for editing your time entries', + ) + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + enhancedDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + enhancedDescription = `${item.description}`; + } } - return `

      ${index + 1}. Date: ${item.date}, Description: ${enhancedDescription}

      `; + return `

      ${index + 1}. Date: ${moment(item.date).format('M-D-YYYY')}, Description: ${enhancedDescription}

      `; }) .join(''); } const administrativeContent = { - startDate: moment(startDate).utc().format('YYYY-MM-DD'), + startDate: moment(startDate).utc().format('M-D-YYYY'), role, userTitle: jobTitle, historyInfringements, @@ -1095,7 +1258,7 @@ const userHelper = function () { personId, { $pull: { - badgeCollection: { _id: mongoose.Types.ObjectId(badgeId) }, + badgeCollection: { badge: mongoose.Types.ObjectId(badgeId) }, }, }, { new: true }, @@ -1166,7 +1329,7 @@ const userHelper = function () { const removePrevHrBadge = async function (personId, user, badgeCollection, hrs, weeks) { // Check each Streak Greater than One to check if it works - if (weeks < 3) { + if (weeks < 2) { return; } let removed = false; @@ -1175,7 +1338,7 @@ const userHelper = function () { { $match: { type: 'X Hours for X Week Streak', - weeks: { $gt: 1, $lt: weeks }, + weeks: { $gt: 0, $lt: weeks }, totalHrs: hrs, }, }, @@ -1196,13 +1359,13 @@ const userHelper = function () { if ( badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && badgeCollection[i].badge?.weeks === bdge.weeks && - bdge.hrs === hrs && + badgeCollection[i].badge?.totalHrs === hrs && !removed ) { changeBadgeCount( personId, badgeCollection[i].badge._id, - badgeCollection[i].badge.count - 1, + badgeCollection[i].count - 1, ); removed = true; return false; @@ -1333,6 +1496,48 @@ const userHelper = function () { }); }; + const getAllWeeksData = async (personId, user) => { + const userId = mongoose.Types.ObjectId(personId); + const weeksData = []; + const currentDate = moment().tz('America/Los_Angeles'); + const startDate = moment(user.createdDate).tz('America/Los_Angeles'); + const numWeeks = Math.ceil(currentDate.diff(startDate, 'days') / 7); + + // iterate through weeks to get hours of each week + for (let week = 1; week <= numWeeks; week += 1) { + const pdtstart = startDate + .clone() + .add(week - 1, 'weeks') + .startOf('week') + .format('YYYY-MM-DD'); + const pdtend = startDate.clone().add(week, 'weeks').subtract(1, 'days').format('YYYY-MM-DD'); + try { + const results = await dashboardHelper.laborthisweek(userId, pdtstart, pdtend); + const { timeSpent_hrs: timeSpent } = results[0]; + weeksData.push(timeSpent); + } catch (error) { + console.error(error); + throw error; + } + } + return weeksData; + }; + + const getMaxHrs = async (personId, user) => { + const weeksdata = await getAllWeeksData(personId, user); + return Math.max(...weeksdata); + }; + + const updatePersonalMax = async (personId, user) => { + try { + const MaxHrs = await getMaxHrs(personId, user); + user.personalBestMaxHrs = MaxHrs; + await user.save(); + } catch (error) { + console.error(error); + } + }; + // 'Personal Max', const checkPersonalMax = async function (personId, user, badgeCollection) { let badgeOfType; @@ -1352,17 +1557,18 @@ const userHelper = function () { } } await badge.findOne({ type: 'Personal Max' }).then((results) => { + const currentDate = moment(moment().format('MM-DD-YYYY'), 'MM-DD-YYYY') + .tz('America/Los_Angeles') + .format('MMM-DD-YY'); if ( user.lastWeekTangibleHrs && - user.lastWeekTangibleHrs >= 1 && - user.lastWeekTangibleHrs === user.personalBestMaxHrs + user.lastWeekTangibleHrs >= user.personalBestMaxHrs && + !badgeOfType.earnedDate.includes(currentDate) ) { if (badgeOfType) { - changeBadgeCount( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - user.personalBestMaxHrs, - ); + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType.badge._id)); + // Update the earnedDate array with the new date + badgeOfType.earnedDate.unshift(moment().format('MMM-DD-YYYY')); } else { addBadge(personId, mongoose.Types.ObjectId(results._id), user.personalBestMaxHrs); } @@ -1435,14 +1641,12 @@ const userHelper = function () { // 'X Hours for X Week Streak', const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { - // Handle Increasing the 1 week streak badges - await checkXHrsInOneWeek(personId, user, badgeCollection); - + let higherBadge = false; // Check each Streak Greater than One to check if it works await badge .aggregate([ { $match: { type: 'X Hours for X Week Streak', weeks: { $gt: 1 } } }, - { $sort: { weeks: -1, totalHrs: -1 } }, + // Group by 'week' property and sorting groups in descending order by 'week', then sorting badges within groups by 'totalHrs' in descending order. { $group: { _id: '$weeks', @@ -1451,6 +1655,41 @@ const userHelper = function () { }, }, }, + { + $project: { + _id: 1, + badges: { + $slice: [ + { + $map: { + input: '$badges', + in: { + _id: '$$this._id', + hrs: '$$this.hrs', + weeks: '$$this.weeks', + }, + }, + }, + { $size: '$badges' }, + ], + }, + }, + }, + { $unwind: '$badges' }, + { $sort: { _id: -1, 'badges.hrs': -1 } }, // Primary sort on _id, secondary sort on badges.hrs + { + $group: { + _id: '$_id', + badges: { + $push: { + _id: '$badges._id', + hrs: '$badges.hrs', + weeks: '$badges.weeks', + }, + }, + }, + }, + { $sort: { _id: -1 } }, // Add this $sort stage for the final sorting by _id ]) .then((results) => { let lastHr = -1; @@ -1487,6 +1726,7 @@ const userHelper = function () { } // if all checks for award badge are green double check that we havent already awarded a higher streak for the same number of hours if (awardBadge && bdge.hrs > lastHr) { + higherBadge = true; lastHr = bdge.hrs; if (badgeOfType && badgeOfType.totalHrs < bdge.hrs) { replaceBadge( @@ -1500,8 +1740,76 @@ const userHelper = function () { addBadge(personId, mongoose.Types.ObjectId(bdge._id)); removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); } else if (badgeOfType && badgeOfType.totalHrs === bdge.hrs) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + const lowerBound = badgeOfType.weeks; + let upperBound; + streak = 0; + + switch (bdge.weeks) { + case 2: + // In between 2Wk and 3Wk + upperBound = 3; + break; + case 3: + // In between 3Wk and 4Wk + upperBound = 4; + break; + case 4: + // In between 4Wk and 6Wk + upperBound = 6; + break; + case 6: + // In between 6Wk and 10Wk + upperBound = 10; + break; + case 10: + // In between 10Wk and 15Wk + upperBound = 15; + break; + case 15: + // In between 50Wk and 20Wk + upperBound = 20; + break; + case 20: + // In between 20Wk and 40Wk + upperBound = 40; + break; + case 40: + // In between 40Wk and 60Wk + upperBound = 60; + break; + case 60: + // In between 60Wk and 80Wk + upperBound = 80; + break; + case 80: + // In between 80Wk and 100Wk + upperBound = 100; + break; + case 100: + // In between 100Wk and 150Wk + upperBound = 150; + break; + case 150: + // In between 150Wk and 200Wk + upperBound = 200; + break; + default: + // Default case. Exiting function. + return; + } + for (let i = endOfArr; i >= endOfArr - upperBound + 1; i -= 1) { + if (user.savedTangibleHrs[i] >= bdge.hrs) { + streak += 1; + } + } + if (streak > lowerBound && streak < upperBound) { + higherBadge = false; + console.log('You are currently building an existing streak, no badge awarded.'); + } else { + console.log('You are currently building a new streak, new badge awarded'); + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); + removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + } } return false; } @@ -1510,6 +1818,9 @@ const userHelper = function () { }); }); }); + + // Handle Increasing the 1 week streak badges + if (!higherBadge) await checkXHrsInOneWeek(personId, user, badgeCollection); }; // 'Lead a team of X+' @@ -1677,6 +1988,7 @@ const userHelper = function () { const { _id, badgeCollection } = user; const personId = mongoose.Types.ObjectId(_id); + await updatePersonalMax(personId, user); await checkPersonalMax(personId, user, badgeCollection); await checkMostHrsWeek(personId, user, badgeCollection); await checkMinHoursMultiple(personId, user, badgeCollection); @@ -1718,8 +2030,30 @@ const userHelper = function () { }); }; + const sendDeactivateEmailBody = function (firstName, lastName, endDate, email, recipients) { + if (endDate) { + const subject = `IMPORTANT:${firstName} ${lastName} has been deactivated in the Highest Good Network`; + const emailBody = `

      Management,

      + +

      Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${endDate}. + Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

      + +

      With Gratitude,

      + +

      One Community

      `; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); + emailSender(recipients, subject, emailBody, null, null, email); + } + }; + const deActivateUser = async () => { try { + const emailReceivers = await userProfile.find( + { isActive: true, role: { $in: ['Owner'] } }, + '_id isActive role email', + ); + const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, '_id isActive endDate', @@ -1729,36 +2063,42 @@ const userHelper = function () { const { endDate } = user; endDate.setHours(endDate.getHours() + 7); if (moment().isAfter(moment(endDate).add(1, 'days'))) { - await userProfile.findByIdAndUpdate( - user._id, - user.set({ - isActive: false, - }), - { new: true }, - ); + try { + await userProfile.findByIdAndUpdate( + user._id, + user.set({ + isActive: false, + }), + { new: true }, + ); + } catch (err) { + // Log the error and continue to the next user + logger.logException(err, `Error in deActivateUser. Failed to update User ${user._id}`); + continue; + } const id = user._id; const person = await userProfile.findById(id); - const lastDay = moment(person.endDate).format('YYYY-MM-DD'); logger.logInfo(`User with id: ${user._id} was de-acticated at ${moment().format()}.`); - - const subject = `IMPORTANT:${person.firstName} ${person.lastName} has been deactivated in the Highest Good Network`; - - const emailBody = `

      Hi Admin!

      - -

      This email is to let you know that ${person.firstName} ${person.lastName} has completed their scheduled last day (${lastDay}) and been deactivated in the Highest Good Network application.

      - -

      This is their email from the system: ${person.email}. Please email them to let them know their work is complete and thank them for their volunteer time with One Community.

      - -

      Thanks!

      - -

      The HGN A.I. (and One Community)

      `; - - emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); + person.teams.map(async (teamId) => { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + }); + sendDeactivateEmailBody( + person.firstName, + person.lastName, + lastDay, + person.email, + recipients, + ); } } } catch (err) { - logger.logException(err); + logger.logException(err, 'Unexpected error in deActivateUser'); } }; @@ -1772,7 +2112,8 @@ const userHelper = function () { try { await token.deleteMany({ isCancelled: true, expiration: { $lt: ninetyDaysAgo } }); } catch (error) { - logger.logException(error); + /* eslint-disable no-undef */ + logger.logException(error, `Error in deleteExpiredTokens. Date ${currentDate}`); } }; @@ -1783,7 +2124,10 @@ const userHelper = function () { try { await timeOffRequest.deleteMany({ endingDate: { $lte: utcEndMoment } }); } catch (error) { - console.error('Error deleting expired time off requests:', error); + logger.logException( + error, + `Error deleting expired time-off requests: utcEndMoment ${utcEndMoment}`, + ); } }; @@ -1791,16 +2135,19 @@ const userHelper = function () { changeBadgeCount, getUserName, getTeamMembers, + getTeamManagementEmail, validateProfilePic, assignBlueSquareForTimeNotMet, applyMissedHourForCoreTeam, deleteBlueSquareAfterYear, reActivateUser, + sendDeactivateEmailBody, deActivateUser, notifyInfringements, getInfringementEmailBody, emailWeeklySummariesForAllUsers, awardNewBadges, + checkXHrsForXWeeks, getTangibleHoursReportedThisWeekByUserId, deleteExpiredTokens, deleteOldTimeOffRequests, diff --git a/src/models/BlueSquareEmailAssignment.js b/src/models/BlueSquareEmailAssignment.js new file mode 100644 index 000000000..d59229e4b --- /dev/null +++ b/src/models/BlueSquareEmailAssignment.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); + +const { Schema } = mongoose; + +const BlueSquareEmailAssignmentSchema = new Schema({ + email: { type: String, required: true, unique: true }, + assignedTo: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile', required: true } +}); + +module.exports = mongoose.model("BlueSquareEmailAssignment", BlueSquareEmailAssignmentSchema, "BlueSquareEmailAssignments"); \ No newline at end of file diff --git a/src/models/bmdashboard/buildingInventoryItem.js b/src/models/bmdashboard/buildingInventoryItem.js index ab8e904ed..77fa2135a 100644 --- a/src/models/bmdashboard/buildingInventoryItem.js +++ b/src/models/bmdashboard/buildingInventoryItem.js @@ -44,7 +44,8 @@ const largeItemBaseSchema = mongoose.Schema({ // actual purchases (once there is a system) may need their own subdoc // subdoc may contain below purchaseStatus and rental fields // for now they have default dummy values - purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], default: 'Rental' }, + purchaseStatus: { type: String, enum: ['Rental', 'Purchase','Needed', 'Purchased'], default: 'Rental' }, + condition: { type: String, enum: ['Like New', 'Good', 'Worn', 'Lost', 'Needs Repair', 'Needs Replacing'], default: 'Like New'}, // TODO: rental fields should be required if purchaseStatus === "Rental" rentedOnDate: { type: Date, default: Date.now() }, rentalDueDate: { type: Date, default: new Date(Date.now() + (3600 * 1000 * 24 * 14)) }, @@ -73,6 +74,7 @@ const largeItemBaseSchema = mongoose.Schema({ responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, type: { type: String, enum: ['Check In', 'Check Out'] }, }], + userResponsible: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, // new field }); const largeItemBase = mongoose.model('largeItemBase', largeItemBaseSchema, 'buildingInventoryItems'); @@ -126,6 +128,11 @@ const buildingReusable = smallItemBase.discriminator('reusable_item', new mongoo const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schema({ // TODO: add function to create simple numeric code for on-site tool tracking code: { type: String, default: '001' }, + purchaseStatus: { + type: String, + enum: ['Rental', 'Purchased'], // Override enum values + default: 'Rental', +} })); //----------------- @@ -138,8 +145,8 @@ const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schem // ex: tractors, excavators, bulldozers const buildingEquipment = largeItemBase.discriminator('equipment_item', new mongoose.Schema({ - isTracked: { type: Boolean, required: true }, // has asset tracker - assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) + // isTracked: { type: Boolean, required: true }, // has asset tracker + // assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) })); module.exports = { diff --git a/src/models/bmdashboard/buildingInventoryType.js b/src/models/bmdashboard/buildingInventoryType.js index 9173bf5cc..7dcaa38dc 100644 --- a/src/models/bmdashboard/buildingInventoryType.js +++ b/src/models/bmdashboard/buildingInventoryType.js @@ -1,7 +1,6 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; - //--------------------------- // BASE INVENTORY TYPE SCHEMA //--------------------------- @@ -13,7 +12,7 @@ const invTypeBaseSchema = new Schema({ name: { type: String, required: true }, description: { type: String, required: true, maxLength: 150 }, imageUrl: String, - createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfiles' }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, }); const invTypeBase = mongoose.model('invTypeBase', invTypeBaseSchema, 'buildingInventoryTypes'); @@ -59,8 +58,23 @@ const reusableType = invTypeBase.discriminator('reusable_type', new mongoose.Sch const toolType = invTypeBase.discriminator('tool_type', new mongoose.Schema({ category: { type: String, enum: ['Tool'] }, - isPowered: { type: Boolean, required: true }, - powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) + invoice: String, + purchaseRental: String, + fromDate: Date, + toDate:Date, + condition: String, + phoneNumber: String, + quantity: Number, + currency: String, + unitPrice: Number, + shippingFee: Number, + taxes: Number, + totalPriceWithShipping: Number, + images: String, + link: String, + + // isPowered: { type: Boolean, required: true }, + // powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) })); //--------------------------- diff --git a/src/models/project.js b/src/models/project.js index 6a78a0b31..da9979628 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -5,9 +5,25 @@ const { Schema } = mongoose; const projectschema = new Schema({ projectName: { type: String, required: true, unique: true }, isActive: { type: Boolean, default: true }, + isArchived: { type: Boolean, default: false }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, - category: { type: String, enum: ['Food', 'Energy', 'Housing', 'Education', 'Society', 'Economics', 'Stewardship', 'Other', 'Unspecified'], default: 'Other' }, + membersModifiedDatetime: { type: Date, default: Date.now() }, + category: { + type: String, + enum: [ + 'Food', + 'Energy', + 'Housing', + 'Education', + 'Society', + 'Economics', + 'Stewardship', + 'Other', + 'Unspecified', + ], + default: 'Other', + }, }); module.exports = mongoose.model('project', projectschema, 'projects'); diff --git a/src/models/team.js b/src/models/team.js index 1c543827b..a92e740b1 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -2,12 +2,6 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; -/** - * This schema represents a team in the system. - * - * Deprecated field: teamCode. Team code is no longer associated with a team. - * Team code is used as a text string identifier in the user profile data model. - */ const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, @@ -17,19 +11,19 @@ const team = new Schema({ { userId: { type: mongoose.SchemaTypes.ObjectId, required: true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, + visible: { type : 'Boolean', default:true}, }, ], - // Deprecated field teamCode: { type: 'String', default: '', validate: { validator(v) { - const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; + const teamCoderegex = /^(.{5,7}|^$)$/; return teamCoderegex.test(v); }, message: - 'Please enter a code in the format of A-AAA or AAAAA', + 'Please enter a code in the format of A-AAAA or AAAAA, with optional numbers, and a total length between 5 and 7 characters.', }, }, }); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index 4ae0b94fa..ea5303b3a 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -15,6 +15,7 @@ const TimeEntry = new Schema({ isTangible: { type: Boolean, default: false }, createdDateTime: { type: Date }, lastModifiedDateTime: { type: Date, default: Date.now }, + isActive: { type: Boolean, default: true }, }); module.exports = mongoose.model('timeEntry', TimeEntry, 'timeEntries'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index f14ef63d3..c3e42b778 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -55,9 +55,7 @@ const userProfileSchema = new Schema({ type: String, required: true, unique: true, - validate: [ - validate({ validator: 'isEmail', message: 'Email address is invalid' }), - ], + validate: [validate({ validator: 'isEmail', message: 'Email address is invalid' })], }, copiedAiPrompt: { type: Date, default: Date.now() }, emailSubscriptions: { @@ -77,7 +75,7 @@ const userProfileSchema = new Schema({ startDate: { type: Date, required: true, - default () { + default() { return this.createdDate; }, }, @@ -140,6 +138,15 @@ const userProfileSchema = new Schema({ country: { type: String, default: '' }, city: { type: String, default: '' }, }, + homeCountry: { + userProvided: { type: String, default: '' }, + coords: { + lat: { type: Number, default: '' }, + lng: { type: Number, default: '' }, + }, + country: { type: String, default: '' }, + city: { type: String, default: '' }, + }, oldInfringements: [ { date: { type: String, required: true }, @@ -219,19 +226,21 @@ const userProfileSchema = new Schema({ ], weeklySummaryNotReq: { type: Boolean, default: false }, timeZone: { type: String, required: true, default: 'America/Los_Angeles' }, - isVisible: { type: Boolean, default: false }, + isVisible: { type: Boolean, default: true }, weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, isFirstTimelog: { type: Boolean, default: true }, + badgeCount: { type: Number, default: 0 }, teamCode: { type: String, default: '', validate: { validator(v) { - const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; + const teamCoderegex = /^(.{5,7}|^$)$/; return teamCoderegex.test(v); }, - message: 'Please enter a code in the format of A-AAA or AAAAA', + message: + 'Please enter a code in the format of A-AAAA or AAAAA, with optional numbers, and a total length between 5 and 7 characters.', }, }, infoCollections: [ @@ -262,11 +271,4 @@ userProfileSchema.pre('save', function (next) { .catch((error) => next(error)); }); -userProfileSchema.index({ teamCode: 1 }); -userProfileSchema.index({ email: 1 }); - -module.exports = mongoose.model( - 'userProfile', - userProfileSchema, - 'userProfiles', -); +module.exports = mongoose.model('userProfile', userProfileSchema, 'userProfiles'); diff --git a/src/models/wbs.js b/src/models/wbs.js index 73f9fd413..bcfbab074 100644 --- a/src/models/wbs.js +++ b/src/models/wbs.js @@ -3,13 +3,11 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const wbsschema = new Schema({ - wbsName: { type: String, required: true }, projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'project' }, isActive: { type: Boolean, default: true }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, - }); module.exports = mongoose.model('wbs', wbsschema, 'wbs'); diff --git a/src/routes/BlueSquareEmailAssignmentRouter.js b/src/routes/BlueSquareEmailAssignmentRouter.js new file mode 100644 index 000000000..0384bb4f8 --- /dev/null +++ b/src/routes/BlueSquareEmailAssignmentRouter.js @@ -0,0 +1,18 @@ +const express = require('express'); + +const routes = function (BlueSquareEmailAssignment,userProfile) { + const BlueSquareEmailAssignmentRouter = express.Router(); + const controller = require('../controllers/BlueSquareEmailAssignmentController')(BlueSquareEmailAssignment,userProfile); + + BlueSquareEmailAssignmentRouter.route('/AssignBlueSquareEmail') + .get(controller.getBlueSquareEmailAssignment) + .post(controller.setBlueSquareEmailAssignment) + + BlueSquareEmailAssignmentRouter.route('/AssignBlueSquareEmail/:id') + .delete(controller.deleteBlueSquareEmailAssignment); + + + return BlueSquareEmailAssignmentRouter; +}; + +module.exports = routes; \ No newline at end of file diff --git a/src/routes/badgeRouter.js b/src/routes/badgeRouter.js index 3f3c8b892..d839813b8 100644 --- a/src/routes/badgeRouter.js +++ b/src/routes/badgeRouter.js @@ -4,17 +4,18 @@ const routes = function (badge) { const controller = require('../controllers/badgeController')(badge); const badgeRouter = express.Router(); - - badgeRouter.route('/badge') - .get(controller.getAllBadges) - .post(controller.postBadge); - badgeRouter.route('/badge/:badgeId') - .delete(controller.deleteBadge) - .put(controller.putBadge); + // badgeRouter.get('/badge/awardBadgesTest', controller.awardBadgesTest); - badgeRouter.route('/badge/assign/:userId') - .put(controller.assignBadges); + badgeRouter.route('/badge').get(controller.getAllBadges).post(controller.postBadge); + + badgeRouter.route('/badge/:badgeId').delete(controller.deleteBadge).put(controller.putBadge); + + badgeRouter.route('/badge/assign/:userId').put(controller.assignBadges); + + badgeRouter.route('/badge/badgecount/:userId').get(controller.getBadgeCount).put(controller.putBadgecount); + + badgeRouter.route('/badge/badgecount/reset/:userId').put(controller.resetBadgecount); return badgeRouter; }; diff --git a/src/routes/bmdashboard/bmEquipmentRouter.js b/src/routes/bmdashboard/bmEquipmentRouter.js index 111d50f77..e97d92cf1 100644 --- a/src/routes/bmdashboard/bmEquipmentRouter.js +++ b/src/routes/bmdashboard/bmEquipmentRouter.js @@ -10,6 +10,8 @@ const routes = function (BuildingEquipment) { equipmentRouter.route('/equipment/purchase').post(controller.bmPurchaseEquipments); + equipmentRouter.route('/equipments').get(controller.fetchBMEquipments); + return equipmentRouter; }; diff --git a/src/routes/bmdashboard/bmInventoryTypeRouter.js b/src/routes/bmdashboard/bmInventoryTypeRouter.js index 3d940ac61..2f57105e5 100644 --- a/src/routes/bmdashboard/bmInventoryTypeRouter.js +++ b/src/routes/bmdashboard/bmInventoryTypeRouter.js @@ -20,10 +20,14 @@ const routes = function (baseInvType, matType, consType, reusType, toolType, equ inventoryTypeRouter.route('/consumables').post(controller.addConsumableType); + inventoryTypeRouter.route('/tools').post(controller.addToolType); + inventoryTypeRouter.route('/invtypes/tools').get(controller.fetchToolTypes); inventoryTypeRouter.route('/invtypes/equipment').post(controller.addEquipmentType); + inventoryTypeRouter.route('/invtypes/equipments').get(controller.fetchEquipmentTypes); + inventoryTypeRouter.route('/invtypes/consumables').get(controller.fetchConsumableTypes); // Route for fetching types by selected type diff --git a/src/routes/bmdashboard/bmReusableRouter.js b/src/routes/bmdashboard/bmReusableRouter.js index a6235823c..2a8c7af52 100644 --- a/src/routes/bmdashboard/bmReusableRouter.js +++ b/src/routes/bmdashboard/bmReusableRouter.js @@ -10,6 +10,12 @@ const routes = function (BuildingReusable) { BuildingReusableController.route('/reusables/purchase') .post(controller.purchaseReusable); + BuildingReusableController.route('/updateReusableRecord') + .post(controller.bmPostReusableUpdateRecord); + + BuildingReusableController.route('/updateReusableRecordBulk') + .post(controller.bmPostReusableUpdateBulk); + return BuildingReusableController; }; diff --git a/src/routes/bmdashboard/bmToolRouter.js b/src/routes/bmdashboard/bmToolRouter.js index e58895433..5a9a96e78 100644 --- a/src/routes/bmdashboard/bmToolRouter.js +++ b/src/routes/bmdashboard/bmToolRouter.js @@ -1,8 +1,11 @@ const express = require('express'); -const routes = function (BuildingTool) { +const routes = function (BuildingTool, ToolType) { const toolRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool); + const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool, ToolType); + + toolRouter.route('/tools') + .get(controller.fetchAllTools); toolRouter.route('/tools/:toolId') .get(controller.fetchSingleTool); @@ -10,6 +13,9 @@ const routes = function (BuildingTool) { toolRouter.route('/tools/purchase') .post(controller.bmPurchaseTools); + toolRouter.route('/tools/log') + .post(controller.bmLogTools); + return toolRouter; }; diff --git a/src/routes/forgotPwdRouter.test.js b/src/routes/forgotPwdRouter.test.js new file mode 100644 index 000000000..ca434433a --- /dev/null +++ b/src/routes/forgotPwdRouter.test.js @@ -0,0 +1,83 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + mockUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections }, + createTestPermissions, + createUser, +} = require('../test'); + +const agent = request.agent(app); + +describe('forgotPwd routes', () => { + let user; + let token; + const reqBody = { + // This is the user we want to create + body: { + ...mockReq.body, + ...mockUser(), + }, + }; + + beforeAll(async () => { + await dbConnect(); + await createTestPermissions(); + user = await createUser(); // This is the requestor user + token = jwtPayload(user); + }); + + beforeEach(async () => { + await dbClearCollections('userProfiles'); + }); + + afterAll(async () => { + await dbClearCollections('userProfiles'); + await dbDisconnect(); + }); + + describe('API routes', () => { + it("should return 404 if route doesn't exists", async () => { + await agent + .post('/api/forgotpasswords') + .send(reqBody.body) + .set('Authorization', token) + .expect(404); + }); + }); + + describe('postForgotPassword', () => { + test('Should return 400 when using findOne for user who does not exists in database', async () => { + // finds user data of a user who does not exists in database + const response = await agent + .post('/api/forgotpassword') + .send(reqBody.body) + .set('Authorization', token) + .expect(400); + + expect(response.body.error).toBe('No Valid user was found'); + }); + + test('Should return 200 when successfully generated a temp password for user', async () => { + // adding a user to the database + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', token) + .expect(200); + + expect(response.body).toBeTruthy(); + + // finds user data of a user who exists in database + response = await agent + .post('/api/forgotpassword') + .send(reqBody.body) + .set('Authorization', token) + .expect(200); + + expect(response.body.message).toBe('generated new password'); + }); + }); +}); diff --git a/src/routes/mapLocationsRouter.test.js b/src/routes/mapLocationsRouter.test.js deleted file mode 100644 index 5eb9a87d1..000000000 --- a/src/routes/mapLocationsRouter.test.js +++ /dev/null @@ -1,200 +0,0 @@ -const request = require('supertest'); -const { jwtPayload } = require('../test'); -const { app } = require('../app'); -const { - mockReq, - createUser, - mongoHelper: { dbConnect, dbDisconnect, dbClearAll }, -} = require('../test'); -const MapLocation = require('../models/mapLocation'); - -const agent = request.agent(app); - -describe('mapLocations routes', () => { - let ownerUser; - let volunteerUser; - let ownerToken; - let volunteerToken; - let reqBody = { - ...mockReq.body, - }; - - beforeAll(async () => { - await dbConnect(); - ownerUser = await createUser(); - volunteerUser = await createUser(); - ownerUser.role = 'Owner'; - volunteerUser.role = 'Volunteer'; - ownerToken = jwtPayload(ownerUser); - volunteerToken = jwtPayload(volunteerUser); - reqBody = { - ...reqBody, - firstName: volunteerUser.firstName, - lastName: volunteerUser.lastName, - jobTitle: 'Software Engineer', - location: { - userProvided: 'A', - coords: { - lat: '51', - lng: '110', - }, - country: 'Test', - city: 'Usa', - }, - _id: volunteerUser._id, - type: 'user', - }; - }); - - afterAll(async () => { - await dbClearAll(); - await dbDisconnect(); - }); - - describe('mapLocationRoutes', () => { - it('should return 401 if authorization header is not present', async () => { - await agent.get('/api/mapLocations').send(reqBody).expect(401); - await agent.put('/api/mapLocations').send(reqBody).expect(401); - await agent.patch('/api/mapLocations').send(reqBody).expect(401); - await agent.delete('/api/mapLocations/123').send(reqBody).expect(401); - }); - - it('should return 404 if the route does not exist', async () => { - await agent - .get('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .put('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .patch('/api/mapLocation') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - await agent - .delete('/api/mapLocation/123') - .set('Authorization', volunteerToken) - .send(reqBody) - .expect(404); - }); - }); - - describe('getMapLocation routes', () => { - it('Should return 200 and the users on success', async () => { - const expected = { - mUsers: [], - users: [ - { - location: { - city: '', - coords: { - lat: 51, - lng: 110, - }, - country: '', - userProvided: '', - }, - isActive: ownerUser.isActive, - jobTitle: ownerUser.jobTitle[0], - _id: ownerUser._id.toString(), - firstName: ownerUser.firstName, - lastName: ownerUser.lastName, - }, - { - location: { - city: '', - coords: { - lat: 51, - lng: 110, - }, - country: '', - userProvided: '', - }, - isActive: volunteerUser.isActive, - jobTitle: volunteerUser.jobTitle[0], - _id: volunteerUser._id.toString(), - firstName: volunteerUser.firstName, - lastName: volunteerUser.lastName, - }, - ], - }; - - const response = await agent - .get('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - expect(response.body).toEqual(expected); - }); - }); - - describe('putMapLocation route', () => { - it('Should return 200 on success', async () => { - const response = await agent - .put('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - const expected = { - _id: expect.anything(), - __v: expect.anything(), - firstName: reqBody.firstName, - lastName: reqBody.lastName, - jobTitle: reqBody.jobTitle, - location: reqBody.location, - isActive: false, - title: 'Prior to HGN Data Collection', - }; - - expect(response.body).toEqual(expected); - }); - }); - - describe('patchMapLocation route', () => { - it('Should return 200 on success', async () => { - reqBody.location.coords.lat = 51; - reqBody.location.coords.lng = 110; - const res = await agent - .patch('/api/mapLocations') - .set('Authorization', ownerToken) - .send(reqBody) - .expect(200); - - const expected = { - firstName: reqBody.firstName, - lastName: reqBody.lastName, - jobTitle: [reqBody.jobTitle], - location: reqBody.location, - _id: reqBody._id.toString(), - type: reqBody.type, - }; - - expect(res.body).toEqual(expected); - }); - }); - - describe('Delete map locations route', () => { - it('Should return 200 on success', async () => { - const _map = new MapLocation(); - _map.firstName = reqBody.firstName; - _map.lastName = reqBody.lastName; - _map.location = reqBody.location; - _map.jobTitle = reqBody.jobTitle; - - const map = await _map.save(); - - const res = await agent - .delete(`/api/mapLocations/${map._id}`) - .set('Authorization', ownerToken) - .send(reqBody); - - expect(res.body).toEqual({ message: 'The location was successfully removed!' }); - }); - }); -}); diff --git a/src/routes/mouseoverTextRouter.test.js b/src/routes/mouseoverTextRouter.test.js new file mode 100644 index 000000000..bbdebf70c --- /dev/null +++ b/src/routes/mouseoverTextRouter.test.js @@ -0,0 +1,98 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + createUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +const MouseoverText = require('../models/mouseoverText'); + +const agent = request.agent(app); + +describe('mouseoverText routes', () => { + let adminUser; + let adminToken; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + adminUser = await createUser(); + adminToken = jwtPayload(adminUser); + }); + + beforeEach(async () => { + await dbClearCollections('mouseoverText'); + reqBody = { + ...reqBody, + newMouseoverText: 'new mouseoverText', + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('mouseoverTextRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/mouseoverText').send(reqBody).expect(401); + await agent.get('/api/mouseoverText').send(reqBody).expect(401); + await agent.put(`/api/mouseoverText/randomId`).send(reqBody).expect(401); + }); + }); + describe('createMouseoverText route', () => { + it('Should return 201 if create new mouseoverText successfully', async () => { + const response = await agent + .post('/api/mouseoverText') + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + + expect(response.body).toEqual({ + mouseoverText: { + _id: expect.anything(), + __v: expect.anything(), + mouseoverText: reqBody.newMouseoverText, + }, + _serverMessage: 'MouseoverText succesfuly created!', + }); + }); + }); + describe('getMouseoverText route', () => { + it('Should return 201 if create new mouseoverText successfully', async () => { + const _mouseoverText = new MouseoverText(); + _mouseoverText.mouseoverText = 'sample mouseoverText'; + await _mouseoverText.save(); + await agent.get('/api/mouseoverText').set('Authorization', adminToken).expect(200); + }); + }); + describe('updateMouseoverText route', () => { + it('Should return 500 if any error in finding mouseoverText by Id', async () => { + reqBody.newMouseoverText = null; + const response = await agent + .put('/api/mouseoverText/randomId') + .send(reqBody) + .set('Authorization', adminToken) + .expect(500); + expect(response.text).toEqual('MouseoverText not found with the given ID'); + }); + it('Should return 201 if updating mouseoverText successfully', async () => { + const _mouseoverText = new MouseoverText(); + _mouseoverText.mouseoverText = 'sample mouseoverText'; + const mouseoverText = await _mouseoverText.save(); + const response = await agent + .put(`/api/mouseoverText/${mouseoverText._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + mouseoverText: reqBody.newMouseoverText, + }); + }); + }); +}); diff --git a/src/routes/reportsRouter.js b/src/routes/reportsRouter.js index 7a98fca8b..a80295ded 100644 --- a/src/routes/reportsRouter.js +++ b/src/routes/reportsRouter.js @@ -1,23 +1,39 @@ /* eslint-disable quotes */ -const express = require("express"); +const express = require('express'); const route = function () { - const controller = require("../controllers/reportsController")(); + const controller = require('../controllers/reportsController')(); const reportsRouter = express.Router(); reportsRouter - .route("/reports/recepients/:userid") + .route('/reports/recepients/:userid') .patch(controller.saveReportsRecepients) .delete(controller.deleteReportsRecepients); + reportsRouter.route('/reports/getrecepients').get(controller.getReportRecipients); + + reportsRouter.route('/reports/weeklysummaries').get(controller.getWeeklySummaries); + + reportsRouter + .route('/reports/overviewsummaries/volunteerstats') + .get(controller.getVolunteerStats); + reportsRouter - .route("/reports/getrecepients") - .get(controller.getReportRecipients); + .route('/reports/overviewsummaries/volunteerhoursstats') + .get(controller.getVolunteerHoursStats); reportsRouter - .route("/reports/weeklysummaries") - .get(controller.getWeeklySummaries); + .route('/reports/overviewsummaries/taskandprojectstats') + .get(controller.getTaskAndProjectStats); + + reportsRouter + .route('/reports/overviewsummaries/volunteerrolestats') + .get(controller.getVolunteerRoleStats); + + reportsRouter.route('/reports/overviewsummaries/bluestats').get(controller.getBlueSquareStats); + + reportsRouter.route('/reports/volunteerstats').get(controller.getVolunteerStatsData); return reportsRouter; }; diff --git a/src/routes/rolePresetRouter.test.js b/src/routes/rolePresetRouter.test.js new file mode 100644 index 000000000..31199be96 --- /dev/null +++ b/src/routes/rolePresetRouter.test.js @@ -0,0 +1,238 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + createUser, + createRole, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +const RolePreset = require('../models/rolePreset'); + +const agent = request.agent(app); + +describe('rolePreset routes', () => { + let adminUser; + let adminToken; + let volunteerUser; + let volunteerToken; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + adminUser = await createUser(); + volunteerUser = await createUser(); + volunteerUser.role = 'Volunteer'; + adminToken = jwtPayload(adminUser); + volunteerToken = jwtPayload(volunteerUser); + // create 2 roles. One with permission and one without + await createRole('Administrator', ['putRole']); + await createRole('Volunteer', []); + }); + + beforeEach(async () => { + await dbClearCollections('rolePreset'); + reqBody = { + ...reqBody, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('rolePresetRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/rolePreset').send(reqBody).expect(401); + await agent.get('/api/rolePreset/randomRoleName').send(reqBody).expect(401); + await agent.put(`/api/rolePreset/randomId`).send(reqBody).expect(401); + await agent.delete('/api/rolePreser/randomId').send(reqBody).expect(401); + }); + }); + + describe('Post rolePreset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if missing roleName', async () => { + reqBody.roleName = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 400 if missing presetName', async () => { + reqBody.presetName = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 400 if missing permissions', async () => { + reqBody.permissions = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 201 if the rolePreset is successfully created', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + + expect(response.body).toEqual({ + newPreset: { + _id: expect.anything(), + __v: expect.anything(), + roleName: reqBody.roleName, + presetName: reqBody.presetName, + permissions: reqBody.permissions, + }, + message: 'New preset created', + }); + }); + }); + + describe('get Presets ByRole route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 200 if getPreset By role successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = 'sample roleName'; + _rolePreset.presetName = 'sample presetName'; + _rolePreset.permissions = ['sample permissions']; + const rolePreset = await _rolePreset.save(); + const response = await agent + .get(`/api/rolePreset/${rolePreset.roleName}`) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual([ + { + _id: expect.anything(), + __v: expect.anything(), + roleName: rolePreset.roleName, + presetName: rolePreset.presetName, + permissions: expect.arrayContaining(rolePreset.permissions), + }, + ]); + }); + }); + describe('update Preset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if the route does not exist', async () => { + await agent + .put('/api/rolePreset/randomId123') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + }); + + it('Should return 200 if update Preset By Id successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = reqBody.roleName; + _rolePreset.presetName = reqBody.presetName; + _rolePreset.permissions = reqBody.permissions; + const rolePreset = await _rolePreset.save(); + const response = await agent + .put(`/api/rolePreset/${rolePreset._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + roleName: reqBody.roleName, + presetName: reqBody.presetName, + permissions: expect.arrayContaining(reqBody.permissions), + }); + }); + }); + describe('delete Preset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if the route does not exist', async () => { + await agent + .delete('/api/rolePreset/randomId123') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + }); + + it('Should return 200 if update Preset By Id successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = reqBody.roleName; + _rolePreset.presetName = reqBody.presetName; + _rolePreset.permissions = reqBody.permissions; + const rolePreset = await _rolePreset.save(); + + const response = await agent + .delete(`/api/rolePreset/${rolePreset._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + message: 'Deleted preset', + }); + }); + }); +}); diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index b404cea0a..4f91dc4b2 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -2,56 +2,42 @@ const express = require('express'); const routes = function (task, userProfile) { const controller = require('../controllers/taskController')(task, userProfile); - const wbsRouter = express.Router(); + const taskRouter = express.Router(); - wbsRouter.route('/tasks/:wbsId/:level/:mother') + taskRouter + .route('/tasks/:wbsId/:level/:mother') .get(controller.getTasks) .put(controller.fixTasks); - wbsRouter.route('/task/:id') - .post(controller.postTask) - .get(controller.getTaskById); + taskRouter.route('/task/:id').post(controller.postTask).get(controller.getTaskById); - wbsRouter.route('/task/import/:id') - .post(controller.importTask); + taskRouter.route('/task/import/:id').post(controller.importTask); - wbsRouter.route('/task/del/:taskId/:mother') - .post(controller.deleteTask); + taskRouter.route('/task/del/:taskId/:mother').post(controller.deleteTask); - wbsRouter.route('/task/wbs/:wbsId') - .get(controller.getWBSId); + taskRouter.route('/task/wbs/:wbsId').get(controller.getWBSId); - wbsRouter.route('/task/wbs/del/:wbsId') - .post(controller.deleteTaskByWBS); + taskRouter.route('/task/wbs/del/:wbsId').post(controller.deleteTaskByWBS); - wbsRouter.route('/task/update/:taskId') - .put(controller.updateTask); + taskRouter.route('/task/update/:taskId').put(controller.updateTask); - wbsRouter.route('/task/updateStatus/:taskId') - .put(controller.updateTaskStatus); + taskRouter.route('/task/updateStatus/:taskId').put(controller.updateTaskStatus); - wbsRouter.route('/task/updateAllParents/:wbsId/') - .put(controller.updateAllParents); + taskRouter.route('/task/updateAllParents/:wbsId/').put(controller.updateAllParents); - wbsRouter.route('/tasks/swap/') - .put(controller.swap); + taskRouter.route('/tasks/swap/').put(controller.swap); - wbsRouter.route('/tasks/update/num') - .put(controller.updateNum); + taskRouter.route('/tasks/update/num').put(controller.updateNum); - wbsRouter.route('/tasks/moveTasks/:wbsId') - .put(controller.moveTask); + taskRouter.route('/tasks/moveTasks/:wbsId').put(controller.moveTask); - wbsRouter.route('/tasks/user/:userId') - .get(controller.getTasksByUserId); + taskRouter.route('/tasks/user/:userId').get(controller.getTasksByUserId); - wbsRouter.route('/user/:userId/teams/tasks') - .get(controller.getTasksForTeamsByUser); + taskRouter.route('/user/:userId/teams/tasks').get(controller.getTasksForTeamsByUser); - wbsRouter.route('/tasks/reviewreq/:userId') - .post(controller.sendReviewReq); + taskRouter.route('/tasks/reviewreq/:userId').post(controller.sendReviewReq); - return wbsRouter; + return taskRouter; }; module.exports = routes; diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index dd940504c..1bf8cfc44 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -5,19 +5,25 @@ const router = function (team) { const teamRouter = express.Router(); - teamRouter.route('/team') + teamRouter + .route('/team') .get(controller.getAllTeams) - .post(controller.postTeam); + .post(controller.postTeam) + .put(controller.updateTeamVisibility); - teamRouter.route('/team/:teamId') + teamRouter + .route('/team/:teamId') .get(controller.getTeamById) .put(controller.putTeam) .delete(controller.deleteTeam); - teamRouter.route('/team/:teamId/users/') + teamRouter + .route('/team/:teamId/users/') .post(controller.assignTeamToUsers) .get(controller.getTeamMembership); + teamRouter.route('/teamCode').get(controller.getAllTeamCode); + return teamRouter; }; diff --git a/src/routes/timeentryRouter.js b/src/routes/timeentryRouter.js index 88f203e94..10ec0d6d0 100644 --- a/src/routes/timeentryRouter.js +++ b/src/routes/timeentryRouter.js @@ -17,6 +17,8 @@ const routes = function (TimeEntry) { TimeEntryRouter.route('/TimeEntry/users').post(controller.getTimeEntriesForUsersList); + TimeEntryRouter.route('/TimeEntry/reports').post(controller.getTimeEntriesForReports); + TimeEntryRouter.route('/TimeEntry/lostUsers').post(controller.getLostTimeEntriesForUserList); TimeEntryRouter.route('/TimeEntry/lostProjects').post( diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 02e9eac9c..e9c458b1b 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -1,9 +1,10 @@ const { body } = require('express-validator'); const express = require('express'); +const { ValidationError } = require('../utilities/errorHandling/customError'); -const routes = function (userProfile) { - const controller = require('../controllers/userProfileController')(userProfile); +const routes = function (userProfile, project) { + const controller = require('../controllers/userProfileController')(userProfile, project); const userProfileRouter = express.Router(); @@ -11,19 +12,31 @@ const routes = function (userProfile) { .route('/userProfile') .get(controller.getUserProfiles) .post( - body('firstName').customSanitizer(value => value.trim()), - body('lastName').customSanitizer(value => value.trim()), + body('firstName').customSanitizer((value) => { + if (!value) throw new ValidationError('First Name is required'); + return value.trim(); + }), + body('lastName').customSanitizer((value) => { + if (!value) throw new ValidationError('Last Name is required'); + return value.trim(); + }), controller.postUserProfile, ); userProfileRouter .route('/userProfile/:userId') .get(controller.getUserById) - .put( - body('firstName').customSanitizer((req) => req.trim()), - body('lastName').customSanitizer((req) => req.trim()), - body('personalLinks').customSanitizer((req) => - req.map((link) => { + .put( + body('firstName').customSanitizer((value) => { + if (!value) throw new ValidationError('First Name is required'); + return value.trim(); + }), + body('lastName').customSanitizer((value) => { + if (!value) throw new ValidationError('Last Name is required'); + return value.trim(); + }), + body('personalLinks').customSanitizer((value) => + value.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, @@ -31,11 +44,11 @@ const routes = function (userProfile) { Link: link.Link.replace(/\s/g, ''), }; } - throw new Error('Url not valid'); + throw new ValidationError('personalLinks not valid'); }), ), - body('adminLinks').customSanitizer((req) => - req.map((link) => { + body('adminLinks').customSanitizer((value) => + value.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, @@ -43,7 +56,7 @@ const routes = function (userProfile) { Link: link.Link.replace(/\s/g, ''), }; } - throw new Error('Url not valid'); + throw new ValidationError('adminLinks not valid'); }), ), controller.putUserProfile, @@ -87,8 +100,7 @@ const routes = function (userProfile) { .route('/userProfile/authorizeUser/weeeklySummaries') .post(controller.authorizeUser); - - userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); + userProfileRouter.route('/userProfile/projects/:name').get(controller.getProjectsByPerson); return userProfileRouter; }; diff --git a/src/routes/wbsRouter.js b/src/routes/wbsRouter.js index a5eb2d126..8a646c757 100644 --- a/src/routes/wbsRouter.js +++ b/src/routes/wbsRouter.js @@ -6,15 +6,9 @@ const routes = function (wbs) { wbsRouter.route('/wbs/:projectId').get(controller.getAllWBS); - wbsRouter.route('/wbs/:id') - .post(controller.postWBS) - .delete(controller.deleteWBS); + wbsRouter.route('/wbs/:id').post(controller.postWBS).delete(controller.deleteWBS); - wbsRouter.route('/wbsId/:id') - .get(controller.getWBSById); - - wbsRouter.route('/wbs/user/:userId') - .get(controller.getWBSByUserId); + wbsRouter.route('/wbsId/:id').get(controller.getWBSById); wbsRouter.route('/wbs').get(controller.getWBS); diff --git a/src/server.js b/src/server.js index 43c6fec6f..e53949703 100644 --- a/src/server.js +++ b/src/server.js @@ -1,14 +1,12 @@ /* eslint-disable quotes */ require('dotenv').load(); -const { app, logger, Sentry } = require('./app'); +const { app, logger } = require('./app'); const websockets = require('./websockets').default; require('./startup/db')(); require('./cronjobs/userProfileJobs')(); -// The error handler must be before any other error middleware and after all controllers -app.use(Sentry.Handlers.errorHandler()); const port = process.env.PORT || 4500; const server = app.listen(port, () => { diff --git a/src/services/userService.js b/src/services/userService.js new file mode 100644 index 000000000..401a32671 --- /dev/null +++ b/src/services/userService.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); +const UserProfileModel = require('../models/userProfile'); +const logger = require('../startup/logger'); +/** + * This function take a list of user email and return a list of user profiles projection that only contains the user ID and user email. + * @param {Array} userEmails A list of user email + * @returns {Array} A list of user profiles projection that only contains the user ID and user email. + */ +async function getUserIdAndEmailByEmails(userEmails) { + if (!Array.isArray(userEmails)) { + throw new Error('Invalid user email list'); + } + try { + return await UserProfileModel.find({ email: { $in: userEmails } }, '_id email'); + } catch (error) { + throw new Error(`Could not fetch user profiles: ${error.message}`); + } +} + +/** + * This function takes a user ID and returns the name of the user. + * @param {*} userId + * @returns {mongoose.Model} The user profile projection contains the first/last name, and email of the user. + */ +async function getUserFullNameAndEmailById(userId) { + try { + return await UserProfileModel.findById(userId, 'firstName lastName email'); + } catch (error) { + logger.logException(error, 'Error getting user full name'); + return null; + } +} + +module.exports = { + getUserIdAndEmailByEmails, + getUserFullNameAndEmailById, +}; diff --git a/src/startup/logger.js b/src/startup/logger.js index 4892a6caa..56d6c5bb1 100644 --- a/src/startup/logger.js +++ b/src/startup/logger.js @@ -1,6 +1,7 @@ /* eslint-disable no-console */ const Sentry = require('@sentry/node'); const { extraErrorDataIntegration } = require('@sentry/integrations'); +const { v4: uuidv4 } = require('uuid'); // Read more about intergration plugins here: https://docs.sentry.io/platforms/node/configuration/integrations/pluggable-integrations/ exports.init = function () { @@ -61,22 +62,33 @@ exports.init = function () { Sentry.setTag('app_name', 'hgn-backend'); }; -exports.logInfo = function (message) { +exports.logInfo = function (message, extraDataObject = null) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment console.log(message); - } else { - Sentry.captureMessage(message, { level: 'info' }); + return 'LocalEnvriomentHasNoTrackingId'; } + return Sentry.captureMessage(message, (scope) => { + scope.setExtras({ extraDataObject }); + scope.setLevel('info'); + return scope; + }); }; /** + * Send log message to Sentry if in production or development environment. Otherwise, log to console. * * @param {Error} error error object to be logged to Sentry * @param {String} transactionName (Optional) name assigned to a transaction. Seachable in Sentry (e.g. error in Function/Service/Operation/Job name) * @param {*} extraData (Optional) extra data to be logged to Sentry (e.g. request body, params, message, etc.) + * @param {String} trackingId (Optional) unique id to track the error in Sentry. Search by tag 'tacking_id' */ -exports.logException = function (error, transactionName = null, extraData = null) { +exports.logException = function ( + error, + transactionName = null, + extraData = null, + trackingId = null, +) { if (process.env.NODE_ENV === 'local' || !process.env.NODE_ENV) { // Do not log to Sentry in local environment console.error(error); @@ -84,6 +96,9 @@ exports.logException = function (error, transactionName = null, extraData = null `Additional info \ntransactionName : ${transactionName} \nextraData: ${JSON.stringify(extraData)}`, ); } else { + if (trackingId == null) { + trackingId = uuidv4(); + } Sentry.captureException(error, (scope) => { if (transactionName !== null) { scope.setTransactionName(transactionName); @@ -91,7 +106,9 @@ exports.logException = function (error, transactionName = null, extraData = null if (extraData !== null) { scope.setExtra('extraData', extraData); } + scope.setTag('tracking_id', trackingId); return scope; }); } + return trackingId; }; diff --git a/src/startup/routes.js b/src/startup/routes.js index 77078bb2c..82a4155a8 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -18,6 +18,7 @@ const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); // Title const title = require('../models/title'); +const blueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); const weeklySummaryAIPrompt = require('../models/weeklySummaryAIPrompt'); const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); @@ -43,11 +44,10 @@ const { buildingTool, buildingEquipment, } = require('../models/bmdashboard/buildingInventoryItem'); -// const buildingTool = require('../models/bmdashboard/buildingTool'); const timeOffRequest = require('../models/timeOffRequest'); const followUp = require('../models/followUp'); -const userProfileRouter = require('../routes/userProfileRouter')(userProfile); +const userProfileRouter = require('../routes/userProfileRouter')(userProfile, project); const warningRouter = require('../routes/warningRouter')(userProfile); const badgeRouter = require('../routes/badgeRouter')(badge); const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); @@ -116,10 +116,14 @@ const bmInventoryTypeRouter = require('../routes/bmdashboard/bmInventoryTypeRout ); const titleRouter = require('../routes/titleRouter')(title); -const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool); +const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool, toolType); const bmEquipmentRouter = require('../routes/bmdashboard/bmEquipmentRouter')(buildingEquipment); const bmIssueRouter = require('../routes/bmdashboard/bmIssueRouter')(buildingIssue); +const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssignmentRouter')( + blueSquareEmailAssignment, + userProfile, +); module.exports = function (app) { app.use('/api', forgotPwdRouter); @@ -157,6 +161,7 @@ module.exports = function (app) { app.use('/api', titleRouter); app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); + app.use('/api', blueSquareEmailAssignmentRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); diff --git a/src/test/assertions.js b/src/test/assertions.js index db12b8cae..cc3bcb900 100644 --- a/src/test/assertions.js +++ b/src/test/assertions.js @@ -1,4 +1,5 @@ const assertResMock = (statusCode, message, response, mockRes) => { + console.log(mockRes); expect(mockRes.status).toHaveBeenCalledWith(statusCode); expect(mockRes.send).toHaveBeenCalledWith(message); expect(response).toBeUndefined(); diff --git a/src/test/createTestPermissions.js b/src/test/createTestPermissions.js index 691f2cd5d..58623ea3f 100644 --- a/src/test/createTestPermissions.js +++ b/src/test/createTestPermissions.js @@ -36,14 +36,18 @@ const permissionsRoles = [ 'putTeam', 'assignTeamToUsers', // Time Entries - 'editTimeEntry', + 'editTimeEntryTime', + 'editTimeEntryDate', + 'editTimeEntryDescription', + 'editTimeEntryToggleTangible', 'deleteTimeEntry', - // 'postTimeEntry',? + 'postTimeEntry', // User Profile 'putRole', 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', + 'updateSummaryRequirements', 'changeUserStatus', 'updatePassword', 'deleteUserProfile', @@ -184,14 +188,19 @@ const permissionsRoles = [ 'deleteTeam', 'putTeam', 'assignTeamToUsers', - 'editTimeEntry', + 'editTimeEntryTime', + 'editTimeEntryDescription', + 'editTimeEntryDate', + 'editTimeEntryToggleTangible', 'deleteTimeEntry', + 'postTimeEntry', 'updatePassword', 'getUserProfiles', 'getProjectMembers', 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', + 'updateSummaryRequirements', 'deleteUserProfile', 'infringementAuthorizer', 'postWbs', diff --git a/src/test/db/createUser.js b/src/test/db/createUser.js index e7c06aebc..ce487ccd6 100644 --- a/src/test/db/createUser.js +++ b/src/test/db/createUser.js @@ -5,9 +5,9 @@ const createUser = async () => { up.password = 'SuperSecretPassword@'; up.role = 'Administrator'; - up.firstName = 'Requestor_first_name'; - up.lastName = 'Requestor_last_name'; - up.jobTitle = ['Any_job_title']; + up.firstName = 'requestor_first_name'; + up.lastName = 'requestor_last_name'; + up.jobTitle = ['any_job_title']; up.phoneNumber = ['123456789']; up.bio = 'any_bio'; up.weeklycommittedHours = 21; @@ -32,8 +32,8 @@ const createUser = async () => { up.location = { userProvided: '', coords: { - lat: 51, - lng: 110, + lat: null, + lng: null, }, country: '', city: '', @@ -46,12 +46,11 @@ const createUser = async () => { up.isFirstTimelog = true; up.actualEmail = ''; up.isVisible = true; - up.totalTangibleHrs = 10; - /* - remove hard coded _id field to allow MongoDB to + /* + remove hard coded _id field to allow MongoDB to automatically create a unique id for us. - Now this function is more reusable if we + Now this function is more reusable if we need to create more than 1 user. */ diff --git a/src/test/mock-response.js b/src/test/mock-response.js index 057ee45d8..336e64057 100644 --- a/src/test/mock-response.js +++ b/src/test/mock-response.js @@ -1,6 +1,7 @@ const mockRes = { status: jest.fn().mockReturnThis(), send: jest.fn(), + json: jest.fn(), }; module.exports = mockRes; diff --git a/src/utilities/addMembersToTeams.js b/src/utilities/addMembersToTeams.js index b637fa2c1..bca402f60 100644 --- a/src/utilities/addMembersToTeams.js +++ b/src/utilities/addMembersToTeams.js @@ -11,21 +11,28 @@ const UserProfile = require('../models/userProfile'); const Teams = require('../models/team'); const addMembersField = async () => { - await Teams.updateMany({}, { $set: { members: [] } }).catch(error => logger.logException('Error adding field:', error)); + await Teams.updateMany({}, { $set: { members: [] } }).catch((error) => + logger.logException('Error adding field:', error), + ); const allUsers = await UserProfile.find({}); const updateOperations = allUsers .map((user) => { const { _id, teams, createdDate } = user; - return teams.map(team => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate } } })); + return teams.map((team) => + Teams.updateOne( + { _id: team }, + { $addToSet: { members: { userId: _id, addDateTime: createdDate, visibility: true } } }, + ), + ); }) .flat(); - await Promise.all(updateOperations).catch(error => logger.logException(error)); + await Promise.all(updateOperations).catch((error) => logger.logException(error)); }; const deleteMembersField = async () => { - await Teams.updateMany({}, { $unset: { members: '' } }).catch(err => console.error(err)); + await Teams.updateMany({}, { $unset: { members: '' } }).catch((err) => console.error(err)); }; const run = () => { @@ -42,7 +49,7 @@ const run = () => { }) // .then(deleteMembersField) .then(addMembersField) - .catch(err => logger.logException(err)) + .catch((err) => logger.logException(err)) .finally(() => { mongoose.connection.close(); console.log('Done! ✅'); diff --git a/src/utilities/constants.js b/src/utilities/constants.js new file mode 100644 index 000000000..5afa9dee0 --- /dev/null +++ b/src/utilities/constants.js @@ -0,0 +1,14 @@ +// Constants used throughout the application. +const constants = { + ALLOWED_EMAIL_ACCOUNT: ['jae@onecommunityglobal.org', 'one.community@me.com', 'jsabol@me.com'], + PROTECTED_EMAIL_ACCOUNT: [ + 'jae@onecommunityglobal.org', + 'one.community@me.com', + 'jsabol@me.com', + 'devadmin@hgn.net', + ], + + // Add more constants here +}; + +module.exports = constants; diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index e0b6560f5..43dfec2a0 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -37,7 +37,10 @@ const permissionsRoles = [ 'putTeam', 'assignTeamToUsers', // Time Entries - 'editTimeEntry', + 'editTimeEntryTime', + 'editTimeEntryDescription', + 'editTimeEntryDate', + 'editTimeEntryToggleTangible', 'deleteTimeEntry', 'postTimeEntry', // User Profile @@ -50,8 +53,10 @@ const permissionsRoles = [ 'updatePassword', 'deleteUserProfile', 'infringementAuthorizer', + 'manageAdminLinks', 'manageTimeOffRequests', 'changeUserRehireableStatus', + 'updateSummaryRequirements', // WBS 'postWbs', 'deleteWbs', @@ -75,7 +80,7 @@ const permissionsRoles = [ 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', - + // Title 'seeQSC', 'addNewTitle', @@ -83,7 +88,6 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'editTeamCode', - ], }, { @@ -202,7 +206,10 @@ const permissionsRoles = [ 'deleteTeam', 'putTeam', 'assignTeamToUsers', - 'editTimeEntry', + 'editTimeEntryTime', + 'editTimeEntryDescription', + 'editTimeEntryDate', + 'editTimeEntryToggleTangible', 'deleteTimeEntry', 'postTimeEntry', 'updatePassword', @@ -211,6 +218,7 @@ const permissionsRoles = [ 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', + 'updateSummaryRequirements', 'deleteUserProfile', 'infringementAuthorizer', 'postWbs', @@ -243,7 +251,7 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'changeUserRehireableStatus', - + 'manageAdminLinks', ], }, ]; @@ -292,7 +300,7 @@ const createInitialPermissions = async () => { } // Update Default presets - const defaultName = 'hard-coded default' + const defaultName = 'hard-coded default'; const presetDataBase = allPresets.find( (preset) => preset.roleName === roleName && preset.presetName === defaultName, diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 655eacaea..eb8eca3de 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -56,15 +56,23 @@ const closure = () => { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(null, result); } - logger.logInfo(result); + // Prevent logging email in production + // Why? + // 1. Could create a security risk + // 2. Could create heavy loads on the server if emails are sent to many people + // 3. Contain limited useful info: + // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} + if (process.env.NODE_ENV === 'local') { + logger.logInfo(`Email sent: ${JSON.stringify(result)}`); + } } catch (error) { if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(error, null); } logger.logException( error, - `Error sending email: from ${CLIENT_EMAIL} to ${recipient}`, - `Extra Data: cc ${cc} bcc ${bcc} subject ${subject}`, + `Error sending email: from ${CLIENT_EMAIL} to ${recipient} subject ${subject}`, + `Extra Data: cc ${cc} bcc ${bcc}`, ); } }, process.env.MAIL_QUEUE_INTERVAL || 1000); diff --git a/src/utilities/errorHandling/customError.js b/src/utilities/errorHandling/customError.js new file mode 100644 index 000000000..81e38f083 --- /dev/null +++ b/src/utilities/errorHandling/customError.js @@ -0,0 +1,48 @@ +/* eslint-disable max-classes-per-file */ +/** + * By throwing an instance of this class, the global error handler middleware will return the error message and status code. + */ +class CustomError extends Error { + statusCode; + + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.name = this.constructor.name; + } +} + +class ValidationError extends CustomError { + constructor(message) { + super(message, 400); + } +} + +class AuthenticationError extends CustomError { + constructor(message) { + super(message, 401); + } +} + +class AuthorizationError extends CustomError { + constructor(message) { + super(message, 403); + } +} + +class RuntimeError extends CustomError { + constructor(message) { + super(message, 500); + } +} + +// Define other error classes here... + +module.exports = { + CustomError, + ValidationError, + AuthenticationError, + AuthorizationError, + RuntimeError, + // Export other error classes here... +}; diff --git a/src/utilities/errorHandling/globalErrorHandler.js b/src/utilities/errorHandling/globalErrorHandler.js new file mode 100644 index 000000000..90d3774f8 --- /dev/null +++ b/src/utilities/errorHandling/globalErrorHandler.js @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ +const { v4: uuidv4 } = require('uuid'); +const { CustomError } = require('./customError'); +const Logger = require('../../startup/logger'); + +/** + * Custom error handler middleware for global unhandled errors. Make it the last middleware since it returns a response and do not call next(). + */ +function globalErrorHandler(err, req, res, next) { + /** + * Notes: + * 1. We will need to implement a global distributed eventId for tracking errors + * if move to microservices artechtecture or with replicated services + * 2. Developer will use the eventId (Searchable) to trace the error in the Sentry.io + */ + const trackingId = uuidv4(); + const errorMessage = `An internal error has occurred. If the issue persists, please contact the administrator and provide the trakcing ID: ${trackingId}`; + + let transactionName = ''; + const requestData = req.body && req.method ? JSON.stringify(req.body) : null; + + if (req.method) { + transactionName = transactionName.concat(req.method); + } + if (req.url) { + transactionName = transactionName.concat(' ', req.originalUrl); + } + + // transactionName = transactionName.concat(' ', 'Tracking ID: ', eventId); + if (!err) { + transactionName = + 'Critical: err parameter is missing. This is probably due to an improper error handling in the code.'; + } + // Log the error to Sentry if not in local environment + if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'production') { + Logger.logException(err, transactionName, requestData, trackingId); + } else { + console.log( + `An error occurred. Transaction: ${transactionName} \nRequest Data: ${requestData}`, + ); + console.error(err); + } + + // If the error is an instance of CustomError, return the error message and status code + if (err instanceof CustomError) { + return res.status(err.statusCode).json({ error: err.message, errorMessage }); + } + + // else return generic error message with tracking id and status code 500 + return res.status(500).json({ + errorMessage, + }); +} + +export default globalErrorHandler; diff --git a/src/utilities/exceptionHandler.js b/src/utilities/exceptionHandler.js deleted file mode 100644 index 9669f362a..000000000 --- a/src/utilities/exceptionHandler.js +++ /dev/null @@ -1,17 +0,0 @@ -const logger = require('../startup/logger'); - -const exceptionHandler = (err, req, res, next) => { - logger.logException(err); - - const errStatus = err.statusCode || 500; - const errMsg = err.message || 'Internal Server Error. Please try again later. If the problem persists, please contact support ID.'; - res.status(errStatus).json({ - success: false, - status: errStatus, - message: errMsg, - stack: !process.env.NODE_ENV || process.env.NODE_ENV === 'local' ? err.stack : {}, - }); - next(); -}; - -export default exceptionHandler; diff --git a/src/utilities/htmlContentSanitizer.js b/src/utilities/htmlContentSanitizer.js index ccfb7cd61..51da82414 100644 --- a/src/utilities/htmlContentSanitizer.js +++ b/src/utilities/htmlContentSanitizer.js @@ -1,8 +1,8 @@ const sanitizeHtml = require('sanitize-html'); // Please refer to https://www.npmjs.com/package/sanitize-html?activeTab=readme for more information. - // eslint-disable-next-line import/prefer-default-export + const cleanHtml = (dirty) => sanitizeHtml(dirty, { allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), @@ -10,4 +10,4 @@ const cleanHtml = (dirty) => module.exports = { cleanHtml, -}; +}; \ No newline at end of file diff --git a/src/utilities/nodeCache.js b/src/utilities/nodeCache.js index 7856a4c55..ac9a93543 100644 --- a/src/utilities/nodeCache.js +++ b/src/utilities/nodeCache.js @@ -41,11 +41,21 @@ const cache = function () { return cacheStore.has(key); } + /** + * Reset or redefine the ttl of a key. If ttl is not passed or set to 0 it's similar to .del() + * @param {*} key + * @param {*} ttl + */ + function setKeyTimeToLive(key, ttl) { + cacheStore.ttl(key, ttl); + } + return { setCache, getCache, removeCache, hasCache, + setKeyTimeToLive, }; }; diff --git a/src/utilities/objectUtils.js b/src/utilities/objectUtils.js new file mode 100644 index 000000000..1c580cfe3 --- /dev/null +++ b/src/utilities/objectUtils.js @@ -0,0 +1,60 @@ +const _ = require('lodash'); + +function deepCopyMongooseObjectWithLodash(originalDoc) { + const plainObject = originalDoc.toObject({ getters: true, virtuals: false }); + const deepCopy = _.cloneDeep(plainObject); + return deepCopy; +} + + + +function filterFieldsFromObj(obj, keysToFilter) { + const filteredObj = {}; + // keys to exclude: sensitive data and verbose data + const keysToExclude = [ + '_id', + '__v', + 'password', + 'location', + 'privacySettings', + 'infringements', + 'badgeCollection', + 'copiedAiPrompt', + 'hoursByCategory', + 'savedTangibleHrs', + ]; + // a list of keys to filter from the object + Object.keys(obj).forEach((key) => { + if (keysToExclude.includes(key)) { + return; + } + if (keysToFilter.includes(key)) { + filteredObj[key] = obj[key]; + } + }); + + return filteredObj; +} + +/** + * Return two objects that have different values for the same key. + * @param {Object} originalDoc Must be a object + * @param {Object} updatedDoc + * @param {Array} keysToFilter + * @returns + */ +function returnObjectDifference(originalDoc, updatedDoc, keysToFilter) { + const originalDocFiltered = filterFieldsFromObj(originalDoc, keysToFilter); + const updatedDocFiltered = filterFieldsFromObj(updatedDoc, keysToFilter); + // filter out the keys that have the same value in both objects + const updatedObj = _.omitBy(updatedDocFiltered, (value, key) => + _.isEqual(value, originalDocFiltered[key]), + ); + const originalObj = _.omitBy(originalDocFiltered, (value, key) => + _.isEqual(value, updatedDocFiltered[key]), + ); + // return an object contains the difference between the original and updated document + return { originalObj, updatedObj }; +} + +module.exports = { deepCopyMongooseObjectWithLodash, filterFieldsFromObj, returnObjectDifference }; diff --git a/src/utilities/permission.spec.js b/src/utilities/permission.spec.js new file mode 100644 index 000000000..579d14988 --- /dev/null +++ b/src/utilities/permission.spec.js @@ -0,0 +1,84 @@ +const { PROTECTED_EMAIL_ACCOUNT } = require('./constants'); +const { canRequestorUpdateUser } = require('./permissions'); +const userService = require('../services/userService'); + +// Mock modules +jest.mock('../startup/logger', () => ({ + logException: jest.fn(), + logInfo: jest.fn(), // Add any other mocked methods if needed +})); +jest.mock('../services/userService'); +jest.mock('./nodeCache', () => + jest.fn().mockImplementation(() => ({ + hasCache: jest.fn(), + getCache: jest.fn(), + setCache: jest.fn(), + })), +); + +// Mock function return +const mockGetUserIdAndEmailByEmails = (value) => + jest + .spyOn(userService, 'getUserIdAndEmailByEmails') + .mockImplementationOnce(() => Promise.resolve(value)); + +describe('canRequestorUpdateUser', () => { + let serverCache; + + beforeEach(() => { + jest.clearAllMocks(); + serverCache = require('./nodeCache')(); + }); + + it('should return true if requestorId is not in protectedEmailAccountIds and targetUserId is also not in protectedEmailAccountIds', async () => { + serverCache.hasCache.mockReturnValue(false); + mockGetUserIdAndEmailByEmails([ + { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, + { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, + { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, + { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, + ]); + + const result = await canRequestorUpdateUser('nonProctedId_1', 'nonProctedId_2'); + expect(result).toBe(true); + }); + + it('should return true if requestorId is in protectedEmailAccountIds and targetUserId is also in protectedEmailAccountIds', async () => { + serverCache.hasCache.mockReturnValue(false); + mockGetUserIdAndEmailByEmails([ + { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, + { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, + { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, + { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, + ]); + + const result = await canRequestorUpdateUser('protectedUserId_1', 'protectedUserId_2'); + expect(result).toBe(true); + }); + + it('should return false if requestorId is not in protectedEmailAccountIds and targetUserId is in protectedEmailAccountIds', async () => { + serverCache.hasCache.mockReturnValue(false); + mockGetUserIdAndEmailByEmails([ + { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, + { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, + { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, + { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, + ]); + + const result = await canRequestorUpdateUser('nonProctedId_1', 'protectedUserId_2'); + expect(result).toBe(false); + }); + + it('should return true if requestorId is in protectedEmailAccountIds and targetUserId is not in protectedEmailAccountIds', async () => { + serverCache.hasCache.mockReturnValue(false); + mockGetUserIdAndEmailByEmails([ + { _id: 'protectedUserId_1', email: PROTECTED_EMAIL_ACCOUNT[0] }, + { _id: 'protectedUserId_2', email: PROTECTED_EMAIL_ACCOUNT[1] }, + { _id: 'protectedUserId_3', email: PROTECTED_EMAIL_ACCOUNT[2] }, + { _id: 'protectedUserId_4', email: PROTECTED_EMAIL_ACCOUNT[3] }, + ]); + + const result = await canRequestorUpdateUser('protectedUserId_2', 'nonProctedId_1'); + expect(result).toBe(true); + }); +}); diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index ff522900e..2299e8812 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -1,36 +1,90 @@ const Role = require('../models/role'); const UserProfile = require('../models/userProfile'); +const serverCache = require('./nodeCache')(); +const userService = require('../services/userService'); +const Logger = require('../startup/logger'); +const { PROTECTED_EMAIL_ACCOUNT, ALLOWED_EMAIL_ACCOUNT } = require('./constants'); -const hasRolePermission = async (role, action) => Role.findOne({ roleName: role }) - .exec() - .then(({ permissions }) => permissions.includes(action)) - .catch(false); +const hasRolePermission = async (role, action) => + Role.findOne({ roleName: role }) + .exec() + .then(({ permissions }) => permissions.includes(action)) + .catch(false); -const hasIndividualPermission = async (userId, action) => UserProfile.findById(userId) - .select('permissions') - .exec() - .then(({ permissions }) => permissions.frontPermissions.includes(action)) - .catch(false); +const hasIndividualPermission = async (userId, action) => + UserProfile.findById(userId) + .select('permissions') + .exec() + .then(({ permissions }) => permissions.frontPermissions.includes(action)) + .catch(false); -const hasPermission = async (requestor, action) => await hasRolePermission(requestor.role, action) || hasIndividualPermission(requestor.requestorId, action); +const hasPermission = async (requestor, action) => + (await hasRolePermission(requestor.role, action)) || + hasIndividualPermission(requestor.requestorId, action); -const canRequestorUpdateUser = (requestorId, userId) => { - const allowedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org - '5baac381e16814009017678c', // dev one.community@me.com - '63fe855b7186de1898fa8ab7', // dev jsabol@me.com - '64deba9064131f13540ac23b', // main jae@onecommunityglobal.org - '610d5ae67002ae3fecdf7080', // main one.community@me.com - '63fe8e4fa79c5619d0b5a563', // main jsabol@me.com - ]; - const protectedIds = ['63feae337186de1898fa8f51', // dev jae@onecommunityglobal.org - '5baac381e16814009017678c', // dev one.community@me.com - '63fe855b7186de1898fa8ab7', // dev jsabol@me.com - '64deba9064131f13540ac23b', // main jae@onecommunityglobal.org - '610d5ae67002ae3fecdf7080', // main one.community@me.com - '63fe8e4fa79c5619d0b5a563', // main jsabol@me.com - '64c17eb8c737b05dd4ac4e28', // dev devadmin@hgn.net - ]; - return !(protectedIds.includes(userId) && !allowedIds.includes(requestorId)); +function getDistinct(arr1, arr2) { + // Merge arrays and reduce to distinct elements + const distinctArray = arr1.concat(arr2).reduce((acc, curr) => { + if (acc.indexOf(curr) === -1) { + acc.push(curr); + } + return acc; + }, []); + + return distinctArray; +} +/** + * Check if requestor can update specific Jae related user. Return false if requestor not allowed to update. Otherwise, return true. + * @param {*} requestorId + * @param {*} userId + * @returns + */ +const canRequestorUpdateUser = async (requestorId, targetUserId) => { + let protectedEmailAccountIds; + let allowedEmailAccountIds; + const emailToQuery = getDistinct(PROTECTED_EMAIL_ACCOUNT, ALLOWED_EMAIL_ACCOUNT); + // Persist the list of protected email accounts in the application cache + if ( + !serverCache.hasCache('protectedEmailAccountIds') || + !serverCache.hasCache('allowedEmailAccountIds') + ) { + try { + // get the user info by email accounts + const query = await userService.getUserIdAndEmailByEmails(emailToQuery); + // Check if all protected email accounts were found + if (query.length !== emailToQuery.length) { + // find out which email accounts were not found + const notFoundEmails = emailToQuery.filter( + (entity) => !query.map(({ email }) => email).includes(entity), + ); + Logger.logInfo( + `The following protected email accounts were not found in the ${process.env.NODE_ENV} database: ${notFoundEmails.join(', ')}.`, + ); + } + // Find out a list of protected email account ids and allowed email id + allowedEmailAccountIds = query + .filter(({ email }) => ALLOWED_EMAIL_ACCOUNT.includes(email)) + .map(({ _id }) => _id); + protectedEmailAccountIds = query + .filter(({ email }) => PROTECTED_EMAIL_ACCOUNT.includes(email)) + .map(({ _id }) => _id); + + serverCache.setCache('protectedEmailAccountIds', protectedEmailAccountIds); + serverCache.setCache('allowedEmailAccountIds', allowedEmailAccountIds); + // Redefine time to live to 1 hour for this specific key + serverCache.setKeyTimeToLive('protectedEmailAccountIds', 60 * 60); + serverCache.setKeyTimeToLive('allowedEmailAccountIds', 60 * 60); + } catch (error) { + Logger.logException(error, 'Error getting protected email accounts'); + } + } else { + protectedEmailAccountIds = serverCache.getCache('protectedEmailAccountIds'); + allowedEmailAccountIds = serverCache.getCache('allowedEmailAccountIds'); + } + // Check requestor edit permission and check target user is protected or not. + return !( + protectedEmailAccountIds.includes(targetUserId) && !allowedEmailAccountIds.includes(requestorId) + ); }; module.exports = { hasPermission, canRequestorUpdateUser }; diff --git a/src/utilities/timeUtils.js b/src/utilities/timeUtils.js index 9239a38de..285f5dce2 100644 --- a/src/utilities/timeUtils.js +++ b/src/utilities/timeUtils.js @@ -26,4 +26,4 @@ module.exports = { formatCreatedDate, DAY_OF_WEEK, getDayOfWeekStringFromUTC, -}; +}; \ No newline at end of file diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 4fed5334c..32c4168f5 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -34,9 +34,10 @@ const action = { PAUSE_TIMER: 'PAUSE_TIMER', STOP_TIMER: 'STOP_TIMER', CLEAR_TIMER: 'CLEAR_TIMER', - SET_GOAL: 'SET_GOAL=', - ADD_GOAL: 'ADD_TO_GOAL=', - REMOVE_GOAL: 'REMOVE_FROM_GOAL=', + GET_TIMER: 'GET_TIMER', + SET_GOAL: 'SET_GOAL', + ADD_GOAL: 'ADD_TO_GOAL', + REMOVE_GOAL: 'REMOVE_FROM_GOAL', FORCED_PAUSE: 'FORCED_PAUSE', ACK_FORCED: 'ACK_FORCED', START_CHIME: 'START_CHIME', @@ -66,6 +67,8 @@ const startTimer = (client) => { }; const pauseTimer = (client, forced = false) => { + if (client.paused) return; + client.time = updatedTimeSinceStart(client); if (client.time === 0) client.chiming = true; client.startAt = moment.invalid(); // invalid can not be saved in database @@ -74,8 +77,8 @@ const pauseTimer = (client, forced = false) => { }; const startChime = (client, msg) => { - const state = msg.split('=')[1]; - client.chiming = state === 'true'; + const state = msg.value; + client.chiming = state === true; }; const ackForcedPause = (client) => { @@ -107,17 +110,27 @@ const clearTimer = (client) => { }; const setGoal = (client, msg) => { - const newGoal = parseInt(msg.split('=')[1]); + const newGoal = parseInt(msg.value); client.goal = newGoal; client.time = newGoal; client.initialGoal = newGoal; }; const addGoal = (client, msg) => { - const duration = parseInt(msg.split('=')[1]); + const duration = parseInt(msg.value); const goalAfterAddition = moment.duration(client.goal).add(duration, 'milliseconds').asHours(); - if (goalAfterAddition > MAX_HOURS) return; + if (goalAfterAddition >= MAX_HOURS) { + const oldGoal = client.goal; + client.goal = MAX_HOURS * 60 * 60 * 1000; + client.time = moment + .duration(client.time) + .add(client.goal - oldGoal, 'milliseconds') + .asMilliseconds() + .toFixed(); + + return; + } client.goal = moment .duration(client.goal) @@ -132,7 +145,7 @@ const addGoal = (client, msg) => { }; const removeGoal = (client, msg) => { - const duration = parseInt(msg.split('=')[1]); + const duration = parseInt(msg.value); const goalAfterRemoval = moment .duration(client.goal) .subtract(duration, 'milliseconds') @@ -157,27 +170,30 @@ const removeGoal = (client, msg) => { }; const handleMessage = async (msg, clients, userId) => { - if (!clients.has(userId)) { - throw new Error('It should have this user in memory'); - } + // if (!clients.has(userId)) { + // throw new Error('It should have this user in memory'); + // } - const client = clients.get(userId); + const client = await getClient(clients, userId); let resp = null; - switch (msg) { + switch (msg.action) { case action.START_TIMER: startTimer(client); break; - case msg.match(/SET_GOAL=/i)?.input: + case action.GET_TIMER: + resp = client; + break; + case action.SET_GOAL: setGoal(client, msg); break; - case msg.match(/ADD_TO_GOAL=/i)?.input: + case action.ADD_GOAL: addGoal(client, msg); break; - case msg.match(/REMOVE_FROM_GOAL=/i)?.input: + case action.REMOVE_GOAL: removeGoal(client, msg); break; - case msg.match(/START_CHIME=/i)?.input: + case action.START_CHIME: startChime(client, msg); break; case action.PAUSE_TIMER: @@ -198,7 +214,7 @@ const handleMessage = async (msg, clients, userId) => { default: resp = { ...client, - error: `Unknown operation ${msg}, please use one from { ${Object.values(action).join(', ')} }`, + error: `Unknown operation ${msg.action}, please use one from { ${Object.values(action).join(', ')} }`, }; break; } diff --git a/src/websockets/index.js b/src/websockets/index.js index a12fa18cb..368f07ba2 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -3,35 +3,31 @@ /* eslint-disable consistent-return */ /* eslint-disable quotes */ /* eslint-disable linebreak-style */ -const WebSocket = require("ws"); -const moment = require("moment"); -const jwt = require("jsonwebtoken"); -const config = require("../config"); +const WebSocket = require('ws'); +const moment = require('moment'); +const jwt = require('jsonwebtoken'); +const config = require('../config'); const { - insertNewUser, - removeConnection, - broadcastToSameUser, - hasOtherConn, -} = require("./TimerService/connectionsHandler"); -const { - getClient, - handleMessage, - action, -} = require("./TimerService/clientsHandler"); + insertNewUser, + removeConnection, + broadcastToSameUser, + hasOtherConn, +} = require('./TimerService/connectionsHandler'); +const { getClient, handleMessage, action } = require('./TimerService/clientsHandler'); /** -* Here we authenticate the user. -* We get the token from the headers and try to verify it. -* If it fails, we throw an error. -* Else we check if the token is valid and if it is, we return the user id. -*/ + * Here we authenticate the user. + * We get the token from the headers and try to verify it. + * If it fails, we throw an error. + * Else we check if the token is valid and if it is, we return the user id. + */ const authenticate = (req, res) => { - const authToken = req.headers?.["sec-websocket-protocol"]; - let payload = ""; + const authToken = req.headers?.['sec-websocket-protocol']; + let payload = ''; try { payload = jwt.verify(authToken, config.JWT_SECRET); } catch (error) { - res("401 Unauthorized", null); + res('401 Unauthorized', null); } if ( @@ -41,34 +37,34 @@ const authenticate = (req, res) => { !payload.role || moment().isAfter(payload.expiryTimestamp) ) { - res("401 Unauthorized", null); + res('401 Unauthorized', null); } res(null, payload.userid); }; /** -* Here we start the timer service. -* First we create a map to store the clients and start the Websockets Server. -* Then we set the upgrade event listener to the Express Server, authenticate the user and -* if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. -*/ + * Here we start the timer service. + * First we create a map to store the clients and start the Websockets Server. + * Then we set the upgrade event listener to the Express Server, authenticate the user and + * if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. + */ export default async (expServer) => { const wss = new WebSocket.Server({ noServer: true, - path: "/timer-service", + path: '/timer-service', }); - expServer.on("upgrade", (request, socket, head) => { + expServer.on('upgrade', (request, socket, head) => { authenticate(request, (err, client) => { if (err || !client) { - socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); socket.destroy(); return; } request.userId = client; wss.handleUpgrade(request, socket, head, (websocket) => { - wss.emit("connection", websocket, request); + wss.emit('connection', websocket, request); }); }); }); @@ -76,11 +72,11 @@ export default async (expServer) => { const clients = new Map(); // { userId: timerInfo } const connections = new Map(); // { userId: connections[] } - wss.on("connection", async (ws, req) => { + wss.on('connection', async (ws, req) => { ws.isAlive = true; const { userId } = req; - ws.on("pong", () => { + ws.on('pong', () => { ws.isAlive = true; }); @@ -93,32 +89,33 @@ export default async (expServer) => { ws.send(JSON.stringify(clientTimer)); /** - * Here we handle the messages from the client. - * And we broadcast the response to all the clients that are connected to the same user. - */ - ws.on("message", async (data) => { - const msg = data.toString(); - if (msg === action.HEARTBEAT) { - ws.send(JSON.stringify({ heartbeat: "pong" })); + * Here we handle the messages from the client. + * And we broadcast the response to all the clients that are connected to the same user. + */ + ws.on('message', async (data) => { + const msg = JSON.parse(data.toString()); + if (msg.action === action.HEARTBEAT) { + ws.send(JSON.stringify({ heartbeat: 'pong' })); return; } - const resp = await handleMessage(msg, clients, userId); + const resp = await handleMessage(msg, clients, msg.userId ?? userId); broadcastToSameUser(connections, userId, resp); + if (msg.userId) broadcastToSameUser(connections, msg.userId, resp); }); /** - * Here we handle the close event. - * If there is another connection to the same user, we don't do anything. - * Else he is the last connection and we do a forced pause if need be. - * This may happen if the user closes all the tabs or the browser or he lost connection with - * the service - * We then remove the connection from the connections map. - */ - ws.on("close", async () => { + * Here we handle the close event. + * If there is another connection to the same user, we don't do anything. + * Else he is the last connection and we do a forced pause if need be. + * This may happen if the user closes all the tabs or the browser or he lost connection with + * the service + * We then remove the connection from the connections map. + */ + ws.on('close', async () => { if (!hasOtherConn(connections, userId, ws)) { const client = clients.get(userId); if (client.started && !client.paused) { - await handleMessage(action.FORCED_PAUSE, clients, userId); + await handleMessage({ action: action.FORCED_PAUSE }, clients, userId); } } removeConnection(connections, userId, ws); From 9088646679c71f304fb11e084df4fec61a115279 Mon Sep 17 00:00:00 2001 From: One Community Date: Thu, 14 Nov 2024 18:17:43 -0800 Subject: [PATCH 03/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_hgn-staging.yml | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/main_hgn-staging.yml diff --git a/.github/workflows/main_hgn-staging.yml b/.github/workflows/main_hgn-staging.yml new file mode 100644 index 000000000..1a1aa02bc --- /dev/null +++ b/.github/workflows/main_hgn-staging.yml @@ -0,0 +1,62 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - hgn-staging + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '18.x' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-staging' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D36DFF5BF4724498838A50C2C0D25D43 }} \ No newline at end of file From 8768a6fb7b6a72a1c90f25bb21b05e720b263c42 Mon Sep 17 00:00:00 2001 From: One Community Date: Thu, 14 Nov 2024 18:26:28 -0800 Subject: [PATCH 04/11] Explicitly disable tests during Azure CI Explicitly disable tests during Azure CI. --- .github/workflows/main_hgn-staging.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main_hgn-staging.yml b/.github/workflows/main_hgn-staging.yml index 1a1aa02bc..806537d73 100644 --- a/.github/workflows/main_hgn-staging.yml +++ b/.github/workflows/main_hgn-staging.yml @@ -25,7 +25,6 @@ jobs: run: | npm install npm run build --if-present - npm run test --if-present - name: Zip artifact for deployment run: zip release.zip ./* -r @@ -59,4 +58,4 @@ jobs: app-name: 'hgn-staging' slot-name: 'Production' package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D36DFF5BF4724498838A50C2C0D25D43 }} \ No newline at end of file + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D36DFF5BF4724498838A50C2C0D25D43 }} From 2a218e45822869e5092a1ec0bb2fcb403f382188 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:25:07 -0800 Subject: [PATCH 05/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_hgn-beta.yml | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/main_hgn-beta.yml diff --git a/.github/workflows/main_hgn-beta.yml b/.github/workflows/main_hgn-beta.yml new file mode 100644 index 000000000..77a254430 --- /dev/null +++ b/.github/workflows/main_hgn-beta.yml @@ -0,0 +1,62 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - hgn-beta + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 'node|14-lts' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-beta' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E8157670AA3448F7AF85E7F1F414EDFA }} \ No newline at end of file From d6a9a70c8c4237677775e07b9c3c0a0cc8cbe95d Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:28:47 -0800 Subject: [PATCH 06/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_hgn-rest.yml | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/main_hgn-rest.yml diff --git a/.github/workflows/main_hgn-rest.yml b/.github/workflows/main_hgn-rest.yml new file mode 100644 index 000000000..75d49d15f --- /dev/null +++ b/.github/workflows/main_hgn-rest.yml @@ -0,0 +1,62 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - hgn-rest + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 'node|14-lts' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D375D726FC884C3C919531718B394AF9 }} \ No newline at end of file From 5d5e527fa2bf2a33c75b9fc422b8a16817bcfce8 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:44:23 -0800 Subject: [PATCH 07/11] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/main_hgn-rest-beta.yml | 87 +++++++++++++----------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/.github/workflows/main_hgn-rest-beta.yml b/.github/workflows/main_hgn-rest-beta.yml index 5db4b0e97..56e7ea0ab 100644 --- a/.github/workflows/main_hgn-rest-beta.yml +++ b/.github/workflows/main_hgn-rest-beta.yml @@ -1,20 +1,6 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - name: Build and deploy Node.js app to Azure Web App - hgn-rest-beta on: @@ -23,35 +9,54 @@ on: - main workflow_dispatch: -env: - AZURE_WEBAPP_NAME: hgn-rest-beta # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - jobs: - build-and-deploy: - name: Build and Deploy + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 'node|14-lts' + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm run test --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: runs-on: ubuntu-latest + needs: build environment: name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_47800FE52B59410A903D5C41C2F9C10F }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest-beta' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_DB4151C3B2C645B88AD84509DA4DD53D }} \ No newline at end of file From a57c2ce4b7d260477e8534a2b421f82e00303003 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:51:31 -0800 Subject: [PATCH 08/11] Update main_hgn-rest-beta.yml --- .github/workflows/main_hgn-rest-beta.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main_hgn-rest-beta.yml b/.github/workflows/main_hgn-rest-beta.yml index 56e7ea0ab..3163b5f58 100644 --- a/.github/workflows/main_hgn-rest-beta.yml +++ b/.github/workflows/main_hgn-rest-beta.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Node.js version uses: actions/setup-node@v3 with: - node-version: 'node|14-lts' + node-version: 14 - name: npm install, build, and test run: | @@ -59,4 +59,4 @@ jobs: app-name: 'hgn-rest-beta' slot-name: 'Production' package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_DB4151C3B2C645B88AD84509DA4DD53D }} \ No newline at end of file + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_DB4151C3B2C645B88AD84509DA4DD53D }} From a02180f3b12639b5c7b518cc701ecbc78dd75e11 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:54:03 -0800 Subject: [PATCH 09/11] Update main_hgn-rest-beta.yml --- .github/workflows/main_hgn-rest-beta.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main_hgn-rest-beta.yml b/.github/workflows/main_hgn-rest-beta.yml index 3163b5f58..24d316a97 100644 --- a/.github/workflows/main_hgn-rest-beta.yml +++ b/.github/workflows/main_hgn-rest-beta.yml @@ -25,7 +25,6 @@ jobs: run: | npm install npm run build --if-present - npm run test --if-present - name: Zip artifact for deployment run: zip release.zip ./* -r From 555a16e63e57ff7ebf4b5b97eab694ac129a91bd Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:55:45 -0800 Subject: [PATCH 10/11] Update main_hgn-rest.yml --- .github/workflows/main_hgn-rest.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main_hgn-rest.yml b/.github/workflows/main_hgn-rest.yml index 75d49d15f..2282de64e 100644 --- a/.github/workflows/main_hgn-rest.yml +++ b/.github/workflows/main_hgn-rest.yml @@ -19,13 +19,12 @@ jobs: - name: Set up Node.js version uses: actions/setup-node@v3 with: - node-version: 'node|14-lts' + node-version: 14 - name: npm install, build, and test run: | npm install npm run build --if-present - npm run test --if-present - name: Zip artifact for deployment run: zip release.zip ./* -r @@ -59,4 +58,4 @@ jobs: app-name: 'hgn-rest' slot-name: 'Production' package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D375D726FC884C3C919531718B394AF9 }} \ No newline at end of file + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D375D726FC884C3C919531718B394AF9 }} From 434192432e8ef2d22a601f52fd221d784e62b6e3 Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:33:22 -0800 Subject: [PATCH 11/11] Delete unused GitHub integration files for Azure. --- .../workflows/development_hgn-rest-dev.yml | 55 ---------------- .github/workflows/main_hgn-beta.yml | 62 ------------------- .github/workflows/main_hgn-staging.yml | 61 ------------------ 3 files changed, 178 deletions(-) delete mode 100644 .github/workflows/development_hgn-rest-dev.yml delete mode 100644 .github/workflows/main_hgn-beta.yml delete mode 100644 .github/workflows/main_hgn-staging.yml diff --git a/.github/workflows/development_hgn-rest-dev.yml b/.github/workflows/development_hgn-rest-dev.yml deleted file mode 100644 index 7d822c869..000000000 --- a/.github/workflows/development_hgn-rest-dev.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - -name: Build and deploy Node.js app to Azure Web App - hgn-rest-dev - -on: - push: - branches: - - development - workflow_dispatch: - -env: - AZURE_WEBAPP_NAME: hgn-rest-dev # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - -jobs: - build-and-deploy: - name: Build and Deploy - runs-on: ubuntu-latest - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D3183F132BC14D79A19DC24953125EA4 }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} diff --git a/.github/workflows/main_hgn-beta.yml b/.github/workflows/main_hgn-beta.yml deleted file mode 100644 index 77a254430..000000000 --- a/.github/workflows/main_hgn-beta.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Node.js app to Azure Web App - hgn-beta - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js version - uses: actions/setup-node@v3 - with: - node-version: 'node|14-lts' - - - name: npm install, build, and test - run: | - npm install - npm run build --if-present - npm run test --if-present - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v4 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp - uses: azure/webapps-deploy@v3 - with: - app-name: 'hgn-beta' - slot-name: 'Production' - package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E8157670AA3448F7AF85E7F1F414EDFA }} \ No newline at end of file diff --git a/.github/workflows/main_hgn-staging.yml b/.github/workflows/main_hgn-staging.yml deleted file mode 100644 index 806537d73..000000000 --- a/.github/workflows/main_hgn-staging.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Node.js app to Azure Web App - hgn-staging - -on: - push: - branches: - - main - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Set up Node.js version - uses: actions/setup-node@v3 - with: - node-version: '18.x' - - - name: npm install, build, and test - run: | - npm install - npm run build --if-present - - - name: Zip artifact for deployment - run: zip release.zip ./* -r - - - name: Upload artifact for deployment job - uses: actions/upload-artifact@v4 - with: - name: node-app - path: release.zip - - deploy: - runs-on: ubuntu-latest - needs: build - environment: - name: 'Production' - url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} - - steps: - - name: Download artifact from build job - uses: actions/download-artifact@v4 - with: - name: node-app - - - name: Unzip artifact for deployment - run: unzip release.zip - - - name: 'Deploy to Azure Web App' - id: deploy-to-webapp - uses: azure/webapps-deploy@v3 - with: - app-name: 'hgn-staging' - slot-name: 'Production' - package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D36DFF5BF4724498838A50C2C0D25D43 }}