From 4f41d5e6e2ddf92744308d2a44304d9ca5a7d1c5 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Mon, 10 Jun 2024 21:06:01 -0400 Subject: [PATCH 01/48] test: completed backend for add/edit/remove warnigns --- src/controllers/currentWarningsController.js | 140 +++++++++++++++++++ src/models/currentWarnings.js | 10 ++ src/routes/curentWarningsRouter.js | 23 +++ src/startup/routes.js | 6 +- 4 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/controllers/currentWarningsController.js create mode 100644 src/models/currentWarnings.js create mode 100644 src/routes/curentWarningsRouter.js diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js new file mode 100644 index 000000000..1c56ceaca --- /dev/null +++ b/src/controllers/currentWarningsController.js @@ -0,0 +1,140 @@ +/* eslint-disable */ +const mongoose = require('mongoose'); +const userProfile = require('../models/userProfile'); + +const currentWarningsController = function (currentWarnings) { + const checkIfSpecialCharacter = (warning) => { + return !/^\b[a-zA-Z]+\b.*$/.test(warning); + }; + const getCurrentWarnings = async (req, res) => { + try { + const response = await currentWarnings.find({}); + + if (response.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + return res.status(201).send({ currentWarningDescriptions: response }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const postNewWarningDescription = async (req, res) => { + try { + const { newWarning, activeWarning } = req.body; + + const newWarningLowerCase = newWarning + .split(' ') + .map((warning) => { + if (!/^\b[a-zA-Z]+\b.*$/.test(warning)) { + throw new Error('warning cannot have special characters as the first letter'); + } + return warning.toLowerCase(); + }) + .join(' '); + + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + + //check to see if it is deactivated or not + //deactaivted warnings should count and duplicates cannot be created + const duplicateFound = warnings.some( + (warning) => warning.warningTitle.toLowerCase() === newWarningLowerCase, + ); + if (duplicateFound) { + return res.status(422).send({ error: 'warning already exists' }); + } + const newWarningDescription = new currentWarnings(); + newWarningDescription.warningTitle = newWarning; + newWarningDescription.activeWarning = activeWarning; + + warnings.push(newWarningDescription); + await newWarningDescription.save(); + + return res.status(201).send({ newWarnings: warnings }); + } catch (error) { + return res.status(401).send({ message: error.message }); + } + }; + + const editWarningDescription = async (req, res) => { + try { + const { editedWarning } = req.body; + + const id = editedWarning._id; + + if (checkIfSpecialCharacter(editedWarning.warningTitle)) { + return res.status(422).send({ + error: 'warning cannot have special characters as the first letter', + }); + } + + await currentWarnings.findOneAndUpdate( + { _id: id }, + [{ $set: { warningTitle: editedWarning.warningTitle.trim() } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + const updateWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + + await currentWarnings.findOneAndUpdate( + { _id: warningDescriptionId }, + [{ $set: { activeWarning: { $not: '$activeWarning' } } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const deleteWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + const documentToDelete = await currentWarnings.findById(warningDescriptionId); + + await currentWarnings.deleteOne({ + _id: mongoose.Types.ObjectId(warningDescriptionId), + }); + + const deletedDescription = documentToDelete.warningTitle; + + await userProfile.updateMany( + { + 'warnings.description': deletedDescription, + }, + { + $pull: { + warnings: { description: deletedDescription }, + }, + }, + ); + + res.status(200).send({ + message: 'warning description was successfully deleted and user profiles updated', + }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + return { + getCurrentWarnings, + postNewWarningDescription, + updateWarningDescription, + deleteWarningDescription, + editWarningDescription, + }; +}; +module.exports = currentWarningsController; diff --git a/src/models/currentWarnings.js b/src/models/currentWarnings.js new file mode 100644 index 000000000..427baf769 --- /dev/null +++ b/src/models/currentWarnings.js @@ -0,0 +1,10 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const currentWarnings = new Schema({ + warningTitle: { type: String, required: true }, + activeWarning: { type: Boolean, required: true }, +}); + +module.exports = mongoose.model('currentWarning', currentWarnings, 'currentWarnings'); diff --git a/src/routes/curentWarningsRouter.js b/src/routes/curentWarningsRouter.js new file mode 100644 index 000000000..f1a004493 --- /dev/null +++ b/src/routes/curentWarningsRouter.js @@ -0,0 +1,23 @@ +const express = require('express'); + +const route = function (currentWarnings) { + const controller = require('../controllers/currentWarningsController')(currentWarnings); + + const currentWarningsRouter = express.Router(); + + currentWarningsRouter + .route('/currentWarnings') + .get(controller.getCurrentWarnings) + .post(controller.postNewWarningDescription); + + currentWarningsRouter.route('/currentWarnings/edit').put(controller.editWarningDescription); + + currentWarningsRouter + .route('/currentWarnings/:warningDescriptionId') + .delete(controller.deleteWarningDescription) + .put(controller.updateWarningDescription); + + return currentWarningsRouter; +}; + +module.exports = route; diff --git a/src/startup/routes.js b/src/startup/routes.js index 51273acbf..8687065d9 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -4,6 +4,7 @@ const project = require('../models/project'); const information = require('../models/information'); const team = require('../models/team'); // const actionItem = require('../models/actionItem'); +// eslint-disable-next-line const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); @@ -16,6 +17,8 @@ const inventoryItemType = require('../models/inventoryItemType'); const role = require('../models/role'); const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); +const currentWarnings = require('../models/currentWarnings'); + // Title const title = require('../models/title'); @@ -48,6 +51,7 @@ const followUp = require('../models/followUp'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const warningRouter = require('../routes/warningRouter')(userProfile); +const currentWarningsRouter = require('../routes/curentWarningsRouter')(currentWarnings); const badgeRouter = require('../routes/badgeRouter')(badge); const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); const timeEntryRouter = require('../routes/timeentryRouter')(timeEntry); @@ -119,7 +123,6 @@ const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool, const bmEquipmentRouter = require('../routes/bmdashboard/bmEquipmentRouter')(buildingEquipment); const bmIssueRouter = require('../routes/bmdashboard/bmIssueRouter')(buildingIssue); - module.exports = function (app) { app.use('/api', forgotPwdRouter); app.use('/api', loginRouter); @@ -153,6 +156,7 @@ module.exports = function (app) { app.use('/api', isEmailExistsRouter); app.use('/api', mapLocationRouter); app.use('/api', warningRouter); + app.use('/api', currentWarningsRouter); app.use('/api', titleRouter); app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); From 74c7850bc2e8793f25875abc5e4c387e08de67b3 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 13 Jun 2024 20:08:15 -0700 Subject: [PATCH 02/48] test: changed get method return code to 200 --- src/controllers/currentWarningsController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index 1c56ceaca..ab0969889 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -13,7 +13,7 @@ const currentWarningsController = function (currentWarnings) { if (response.length === 0) { return res.status(400).send({ message: 'no valid records' }); } - return res.status(201).send({ currentWarningDescriptions: response }); + return res.status(200).send({ currentWarningDescriptions: response }); } catch (error) { res.status(401).send({ message: error.message || error }); } From 7eb0a65e992b978d84f38268a243e1c2a2f79c99 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 13 Jun 2024 21:26:46 -0700 Subject: [PATCH 03/48] test: added missing code for backend to update with new warnings --- src/controllers/warningsController.js | 60 ++++++++++++++------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index c71e22f0d..a5461fba7 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -1,21 +1,34 @@ +/* eslint-disable */ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); +const currentWarnings = require('../models/currentWarnings'); +let currentWarningDescriptions = null; + +async function getWarningDescriptions() { + currentWarningDescriptions = await currentWarnings.find({}, { warningTitle: 1, _id: 0 }); +} + +const convertObjectToArray = (obj) => { + const arr = []; + for (const key of obj) { + arr.push(key.warningTitle); + } + return arr; +}; -const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', -]; const warningsController = function (UserProfile) { const getWarningsByUserId = async function (req, res) { + currentWarningDescriptions = await currentWarnings.find({ + activeWarning: true, + }); + + currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); const { userId } = req.params; try { const { warnings } = await UserProfile.findById(userId); - const completedData = filterWarnings(warnings); + const completedData = filterWarnings(currentWarningDescriptions, warnings); if (!warnings) { return res.status(400).send({ message: 'no valiud records' }); @@ -30,9 +43,7 @@ const warningsController = function (UserProfile) { try { const { userId } = req.params; - const { - iconId, color, date, description, -} = req.body; + const { iconId, color, date, description } = req.body; const record = await UserProfile.findById(userId); if (!record) { @@ -46,9 +57,10 @@ const warningsController = function (UserProfile) { date, description, }); + await record.save(); - const completedData = filterWarnings(record.warnings); + const completedData = filterWarnings(currentWarningDescriptions, record.warnings); res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { @@ -71,10 +83,8 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'no valid records' }); } - const sortedWarnings = filterWarnings(warnings.warnings); - res - .status(201) - .send({ message: 'succesfully deleted', warnings: sortedWarnings }); + const sortedWarnings = filterWarnings(currentWarningDescriptions, warnings.warnings); + res.status(201).send({ message: 'succesfully deleted', warnings: sortedWarnings }); } catch (error) { res.status(401).send({ message: error.message || error }); } @@ -88,18 +98,10 @@ const warningsController = function (UserProfile) { }; // gests the dsecriptions key from the array -const getDescriptionKey = (val) => { - const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', - ]; - - return descriptions.indexOf(val); -}; +const getDescriptionKey = (val) => + // currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); + currentWarningDescriptions.indexOf(val); const sortKeysAlphabetically = (a, b) => getDescriptionKey(a) - getDescriptionKey(b); // method to see which color is first @@ -122,7 +124,7 @@ const sortByColorAndDate = (a, b) => { return colorComparison; }; -const filterWarnings = (warnings) => { +const filterWarnings = (currentWarningDescriptions, warnings) => { const warningsObject = {}; warnings.forEach((warning) => { @@ -145,7 +147,7 @@ const filterWarnings = (warnings) => { const completedData = []; - for (const descrip of descriptions) { + for (const descrip of currentWarningDescriptions) { completedData.push({ title: descrip, warnings: warns[descrip] ? warns[descrip] : [], From bd8c58dd159b75600444a4508cae414833e01961 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Wed, 19 Jun 2024 20:28:34 -0700 Subject: [PATCH 04/48] test: completed editing and adding bugs --- src/controllers/currentWarningsController.js | 55 +++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index ab0969889..bffb46ae9 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -3,8 +3,16 @@ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const currentWarningsController = function (currentWarnings) { + const checkForDuplicates = (currentWarning, warnings) => { + const duplicateFound = warnings.some( + (warning) => warning.warningTitle.toLowerCase() === currentWarning, + ); + + return duplicateFound; + }; + const checkIfSpecialCharacter = (warning) => { - return !/^\b[a-zA-Z]+\b.*$/.test(warning); + return !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(warning); }; const getCurrentWarnings = async (req, res) => { try { @@ -23,30 +31,24 @@ const currentWarningsController = function (currentWarnings) { try { const { newWarning, activeWarning } = req.body; - const newWarningLowerCase = newWarning - .split(' ') - .map((warning) => { - if (!/^\b[a-zA-Z]+\b.*$/.test(warning)) { - throw new Error('warning cannot have special characters as the first letter'); - } - return warning.toLowerCase(); - }) - .join(' '); - const warnings = await currentWarnings.find({}); if (warnings.length === 0) { return res.status(400).send({ message: 'no valid records' }); } - //check to see if it is deactivated or not - //deactaivted warnings should count and duplicates cannot be created - const duplicateFound = warnings.some( - (warning) => warning.warningTitle.toLowerCase() === newWarningLowerCase, - ); - if (duplicateFound) { + const lowerCaseWarning = newWarning.toLowerCase(); + const testWarning = !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(lowerCaseWarning); + if (testWarning) { + return res.status(422).send({ + error: 'Warning cannot have special characters as the first letter', + }); + } + + if (checkForDuplicates(lowerCaseWarning, warnings)) { return res.status(422).send({ error: 'warning already exists' }); } + const newWarningDescription = new currentWarnings(); newWarningDescription.warningTitle = newWarning; newWarningDescription.activeWarning = activeWarning; @@ -66,15 +68,28 @@ const currentWarningsController = function (currentWarnings) { const id = editedWarning._id; - if (checkIfSpecialCharacter(editedWarning.warningTitle)) { + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + + const lowerCaseWarning = editedWarning.warningTitle.toLowerCase(); + const testWarning = !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(lowerCaseWarning); + + if (testWarning) { return res.status(422).send({ - error: 'warning cannot have special characters as the first letter', + error: 'Warning cannot have special characters as the first letter', }); } + if (checkForDuplicates(lowerCaseWarning, warnings)) { + return res.status(422).send({ error: 'warning already exists try a different name' }); + } + await currentWarnings.findOneAndUpdate( { _id: id }, - [{ $set: { warningTitle: editedWarning.warningTitle.trim() } }], + [{ $set: { warningTitle: lowerCaseWarning.trim() } }], { new: true }, ); From 301fd28994838ed43993df41e41bf5b1c016d7a0 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Wed, 19 Jun 2024 20:34:24 -0700 Subject: [PATCH 05/48] feat: used checkIfspecial function to check against regex --- src/controllers/currentWarningsController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index bffb46ae9..1dfd67fa0 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -38,7 +38,7 @@ const currentWarningsController = function (currentWarnings) { } const lowerCaseWarning = newWarning.toLowerCase(); - const testWarning = !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(lowerCaseWarning); + const testWarning = !checkIfSpecialCharacter(lowerCaseWarning); if (testWarning) { return res.status(422).send({ error: 'Warning cannot have special characters as the first letter', @@ -75,7 +75,7 @@ const currentWarningsController = function (currentWarnings) { } const lowerCaseWarning = editedWarning.warningTitle.toLowerCase(); - const testWarning = !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(lowerCaseWarning); + const testWarning = !checkIfSpecialCharacter(lowerCaseWarning); if (testWarning) { return res.status(422).send({ From c1d6c76c7c1be99b77315955af1bcb5999a2c6c3 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 20 Jun 2024 20:48:45 -0700 Subject: [PATCH 06/48] fix: fixed bug that prevented numbers in new warning and edited warning --- src/controllers/currentWarningsController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index 1dfd67fa0..84c0e4ba6 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -38,7 +38,7 @@ const currentWarningsController = function (currentWarnings) { } const lowerCaseWarning = newWarning.toLowerCase(); - const testWarning = !checkIfSpecialCharacter(lowerCaseWarning); + const testWarning = checkIfSpecialCharacter(lowerCaseWarning); if (testWarning) { return res.status(422).send({ error: 'Warning cannot have special characters as the first letter', @@ -75,7 +75,7 @@ const currentWarningsController = function (currentWarnings) { } const lowerCaseWarning = editedWarning.warningTitle.toLowerCase(); - const testWarning = !checkIfSpecialCharacter(lowerCaseWarning); + const testWarning = checkIfSpecialCharacter(lowerCaseWarning); if (testWarning) { return res.status(422).send({ From 161b2c1ee1179ec79cd6a6912a46295544670908 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Mon, 12 Aug 2024 22:32:31 -0700 Subject: [PATCH 07/48] fix: fixed the lowercase issue and the 422 error code, also, added code to prevent post method from crashing --- src/controllers/currentWarningsController.js | 9 ++++----- src/controllers/warningsController.js | 18 ++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index 84c0e4ba6..ecf35c3a2 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -37,16 +37,15 @@ const currentWarningsController = function (currentWarnings) { return res.status(400).send({ message: 'no valid records' }); } - const lowerCaseWarning = newWarning.toLowerCase(); - const testWarning = checkIfSpecialCharacter(lowerCaseWarning); + const testWarning = checkIfSpecialCharacter(newWarning); if (testWarning) { - return res.status(422).send({ + return res.status(200).send({ error: 'Warning cannot have special characters as the first letter', }); } - if (checkForDuplicates(lowerCaseWarning, warnings)) { - return res.status(422).send({ error: 'warning already exists' }); + if (checkForDuplicates(newWarning, warnings)) { + return res.status(200).send({ error: 'warning already exists' }); } const newWarningDescription = new currentWarnings(); diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index a5461fba7..0f80d2a8d 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -50,17 +50,15 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'No valid records found' }); } - record.warnings = record.warnings.concat({ - userId, - iconId, - color, - date, - description, - }); - - await record.save(); + const updatedWarnings = await userProfile.findByIdAndUpdate( + { + _id: userId, + }, + { $push: { warnings: { userId, iconId, color, date, description } } }, + { new: true, upsert: true }, + ); - const completedData = filterWarnings(currentWarningDescriptions, record.warnings); + const completedData = filterWarnings(currentWarningDescriptions, updatedWarnings.warnings); res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { From fa01786cacdaaa6351728d440c7a7852977fa17e Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Tue, 13 Aug 2024 20:33:33 -0700 Subject: [PATCH 08/48] fix: added 200 response code instead of 422 for posting, editing a warning --- src/controllers/currentWarningsController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index ecf35c3a2..a1f62cfc6 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -77,13 +77,13 @@ const currentWarningsController = function (currentWarnings) { const testWarning = checkIfSpecialCharacter(lowerCaseWarning); if (testWarning) { - return res.status(422).send({ + return res.status(200).send({ error: 'Warning cannot have special characters as the first letter', }); } if (checkForDuplicates(lowerCaseWarning, warnings)) { - return res.status(422).send({ error: 'warning already exists try a different name' }); + return res.status(200).send({ error: 'warning already exists try a different name' }); } await currentWarnings.findOneAndUpdate( From c5e78654a6b967c8537a55600fce812511fc1c8b Mon Sep 17 00:00:00 2001 From: One Community Date: Wed, 14 Aug 2024 20:41:15 -0700 Subject: [PATCH 09/48] 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 10/48] 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 ab1e953d43890c0c4d5baca24dd3e13d44bfb02b Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Wed, 21 Aug 2024 20:04:59 -0700 Subject: [PATCH 11/48] fix: removed logs and added isPermanent Attribute to new warnings --- src/controllers/currentWarningsController.js | 3 ++- src/models/currentWarnings.js | 1 + src/models/userProfile.js | 12 +++--------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index a1f62cfc6..f064cb4f7 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -29,7 +29,7 @@ const currentWarningsController = function (currentWarnings) { const postNewWarningDescription = async (req, res) => { try { - const { newWarning, activeWarning } = req.body; + const { newWarning, activeWarning, isPermanent } = req.body; const warnings = await currentWarnings.find({}); @@ -51,6 +51,7 @@ const currentWarningsController = function (currentWarnings) { const newWarningDescription = new currentWarnings(); newWarningDescription.warningTitle = newWarning; newWarningDescription.activeWarning = activeWarning; + newWarningDescription.isPermanent = isPermanent; warnings.push(newWarningDescription); await newWarningDescription.save(); diff --git a/src/models/currentWarnings.js b/src/models/currentWarnings.js index 427baf769..18a446199 100644 --- a/src/models/currentWarnings.js +++ b/src/models/currentWarnings.js @@ -5,6 +5,7 @@ const { Schema } = mongoose; const currentWarnings = new Schema({ warningTitle: { type: String, required: true }, activeWarning: { type: Boolean, required: true }, + isPermanent: { type: Boolean, required: true }, }); module.exports = mongoose.model('currentWarning', currentWarnings, 'currentWarnings'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 369708e4c..e6e9e51aa 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: function () { + default() { return this.createdDate; }, }, @@ -262,8 +260,4 @@ userProfileSchema.pre('save', function (next) { .catch((error) => next(error)); }); -module.exports = mongoose.model( - 'userProfile', - userProfileSchema, - 'userProfiles', -); +module.exports = mongoose.model('userProfile', userProfileSchema, 'userProfiles'); From da17641c20b8999777707315a23b4eac5619979d Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Wed, 21 Aug 2024 21:53:11 -0700 Subject: [PATCH 12/48] fix: attempted to send email to my email address to test --- src/cronjobs/userProfileJobs.js | 5 +++-- src/helpers/userHelper.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index f0f69e146..8acd192f3 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -5,12 +5,13 @@ const userhelper = require('../helpers/userHelper')(); const userProfileJobs = () => { const allUserProfileJobs = new CronJob( - // '* * * * *', // Comment out for testing. Run Every minute. - '1 0 * * 0', // Every Sunday, 1 minute past midnight. + '* * * * *', // Comment out for testing. Run Every minute. + // '1 0 * * 0', // Every Sunday, 1 minute past midnight. async () => { const SUNDAY = 0; // will change back to 0 after fix if (moment().tz('America/Los_Angeles').day() === SUNDAY) { + console.log('Running Cron Jobs'); await userhelper.assignBlueSquareForTimeNotMet(); await userhelper.applyMissedHourForCoreTeam(); await userhelper.emailWeeklySummariesForAllUsers(); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index a2bd94117..1f1ad9030 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -401,7 +401,7 @@ const userHelper = function () { const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); const users = await userProfile.find( - { isActive: true }, + { isActive: true, email: 'arevaloluis114@gmail.com' }, '_id weeklycommittedHours weeklySummaries missedHours', ); const usersRequiringBlueSqNotification = []; @@ -731,7 +731,7 @@ const userHelper = function () { 'New Infringement Assigned', emailBody, null, - 'onecommunityglobal@gmail.com', + 'arevaloluis114@gmail.com', status.email, null, ); From 78ee068e13a8d5586e1967df12806504ed846c8f Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Tue, 10 Sep 2024 20:55:17 -0700 Subject: [PATCH 13/48] feat: added send an email after 3 and 4 warnings function --- src/controllers/warningsController.js | 120 ++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 0f80d2a8d..9a2541791 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -2,8 +2,27 @@ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const currentWarnings = require('../models/currentWarnings'); -let currentWarningDescriptions = null; +const emailSender = require('../utilities/emailSender'); +let currentWarningDescriptions = null; +let currentUserName = null; +const emailTemplate = { + thirdWarning: { + subject: 'Third Warning', + body: `

      This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      +

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      +

      With Gratitude,

      +

      One Community

      `, + }, + fourthWarning: { + subject: 'Fourth Warning', + body: `

      username !

      +

      This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      +

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      +

      With Gratitude,

      +

      One Community

      `, + }, +}; async function getWarningDescriptions() { currentWarningDescriptions = await currentWarnings.find({}, { warningTitle: 1, _id: 0 }); } @@ -28,7 +47,7 @@ const warningsController = function (UserProfile) { try { const { warnings } = await UserProfile.findById(userId); - const completedData = filterWarnings(currentWarningDescriptions, warnings); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings); if (!warnings) { return res.status(400).send({ message: 'no valiud records' }); @@ -44,12 +63,20 @@ const warningsController = function (UserProfile) { const { userId } = req.params; const { iconId, color, date, description } = req.body; + const { monitorData } = req.body; + const myData = { + firstName: 'tim', + lastName: 'smith', + email: 'tim@gmail.com', + }; const record = await UserProfile.findById(userId); if (!record) { return res.status(400).send({ message: 'No valid records found' }); } + currentUserName = `${record.firstName} ${record.lastName}`; + //check warning id const updatedWarnings = await userProfile.findByIdAndUpdate( { _id: userId, @@ -58,7 +85,15 @@ const warningsController = function (UserProfile) { { new: true, upsert: true }, ); - const completedData = filterWarnings(currentWarningDescriptions, updatedWarnings.warnings); + const { completedData, sendEmail } = filterWarnings( + currentWarningDescriptions, + updatedWarnings.warnings, + description, + iconId, + ); + if (sendEmail !== null) { + sendEmailToUser(sendEmail, description, currentUserName, monitorData); + } res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { @@ -81,8 +116,8 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'no valid records' }); } - const sortedWarnings = filterWarnings(currentWarningDescriptions, warnings.warnings); - res.status(201).send({ message: 'succesfully deleted', warnings: sortedWarnings }); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings.warnings); + res.status(201).send({ message: 'succesfully deleted', warnings: completedData }); } catch (error) { res.status(401).send({ message: error.message || error }); } @@ -95,6 +130,41 @@ const warningsController = function (UserProfile) { }; }; +const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitorData) => { + let time = sendEmail === '3' ? '3rd' : '4th'; + console.log('monitorData', monitorData.firstName); + const emailTemplate = + time === '3rd' + ? ` +

      Hello ${currentUserName},

      +

      This is the ${time} time the Admin team has requested the same thing from you. Specifically ${warningDescription}. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      +

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      +

      The Admin memember who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}

      +

      With Gratitude,

      +

      One Community

      ` + : `

      Hello ${currentUserName},

      +

      This is the ${time} time the Admin team has requested the same thing from you. Specifically ${warningDescription}. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      +

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      +

      The Admin memember who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}

      +

      With Gratitude,

      +

      One Community

      `; + + if (sendEmail === '3') { + emailSender('arevaloluis114@gmail.com', 'Third Warning', emailTemplate, null, null); + } else { + emailSender('arevaloluis114@gmail.com', 'Fourth Warning', emailTemplate, null, null); + } + + // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.thirdWarning.body}`; + // emailSender( + // 'arevaloluis114@gmail.com', + // emailTemplate.thirdWarning.subject, + // emailBody, + // null, + // null, + // ); +}; + // gests the dsecriptions key from the array const getDescriptionKey = (val) => // currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); @@ -122,14 +192,50 @@ const sortByColorAndDate = (a, b) => { return colorComparison; }; -const filterWarnings = (currentWarningDescriptions, warnings) => { +const filterWarnings = ( + currentWarningDescriptions, + warnings, + description = null, + iconId = null, +) => { const warningsObject = {}; + let sendEmail = null; + warnings.forEach((warning) => { if (!warningsObject[warning.description]) { warningsObject[warning.description] = []; } warningsObject[warning.description].push(warning); + + if ( + warningsObject[warning.description].length === 3 && + description === warning.description && + sendEmail === null + ) { + sendEmail = '3'; + //send email + // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.thirdWarning.body}`; + // emailSender( + // 'arevaloluis114@gmail.com', + // emailTemplate.thirdWarning.subject, + // emailBody, + // null, + // null, + // ); + } else if ( + warningsObject[warning.description].length === 4 && + description === warning.description && + sendEmail === null + ) { + sendEmail = '4'; + //send email + // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.fourthWarning.body}`; + // emailSender( + // ' + } else { + sendEmail = null; + } }); const warns = Object.keys(warningsObject) @@ -151,7 +257,7 @@ const filterWarnings = (currentWarningDescriptions, warnings) => { warnings: warns[descrip] ? warns[descrip] : [], }); } - return completedData; + return { completedData, sendEmail }; }; module.exports = warningsController; From 91e839861ca13f08fe94c3d2dc599929c15bca10 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Wed, 18 Sep 2024 15:14:17 -0400 Subject: [PATCH 14/48] feat: added sending email features and adjusted the user profile mode --- src/controllers/warningsController.js | 120 +++++++++++--------------- src/models/userProfile.js | 1 + 2 files changed, 49 insertions(+), 72 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 9a2541791..8ad00ab8f 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -65,11 +65,6 @@ const warningsController = function (UserProfile) { const { iconId, color, date, description } = req.body; const { monitorData } = req.body; - const myData = { - firstName: 'tim', - lastName: 'smith', - email: 'tim@gmail.com', - }; const record = await UserProfile.findById(userId); if (!record) { return res.status(400).send({ message: 'No valid records found' }); @@ -77,7 +72,7 @@ const warningsController = function (UserProfile) { currentUserName = `${record.firstName} ${record.lastName}`; //check warning id - const updatedWarnings = await userProfile.findByIdAndUpdate( + const updatedWarnings = await UserProfile.findByIdAndUpdate( { _id: userId, }, @@ -85,18 +80,20 @@ const warningsController = function (UserProfile) { { new: true, upsert: true }, ); - const { completedData, sendEmail } = filterWarnings( + const { completedData, sendEmail, size } = filterWarnings( currentWarningDescriptions, updatedWarnings.warnings, - description, iconId, + color, ); + if (sendEmail !== null) { - sendEmailToUser(sendEmail, description, currentUserName, monitorData); + sendEmailToUser(sendEmail, description, currentUserName, monitorData, size); } res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { + console.log('error', error); res.status(400).send({ message: error.message || error }); } }; @@ -130,42 +127,42 @@ const warningsController = function (UserProfile) { }; }; -const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitorData) => { - let time = sendEmail === '3' ? '3rd' : '4th'; - console.log('monitorData', monitorData.firstName); +//helper function to get the ordinal +function getOrdinal(n) { + const suffixes = ['th', 'st', 'nd', 'rd']; + const value = n % 100; + return n + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]); +} +const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitorData, size) => { + //issued blue square? if so, send second tempalte + // + const ordinal = getOrdinal(size); + const subjectTitle = ordinal + ' Warning'; + const emailTemplate = - time === '3rd' - ? ` -

      Hello ${currentUserName},

      -

      This is the ${time} time the Admin team has requested the same thing from you. Specifically ${warningDescription}. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      -

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      -

      The Admin memember who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}

      -

      With Gratitude,

      -

      One Community

      ` + sendEmail === 'issue warning' + ? `

      Hello ${currentUserName},

      +

      This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}. Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, don’t hesitate to ask questions—the Admin team is here to assist.

      +

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention and may result in a blue square if it happens again.

      +

      The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them via email if you have any questions.

      +

      With Gratitude,

      +

      One Community

      ` : `

      Hello ${currentUserName},

      -

      This is the ${time} time the Admin team has requested the same thing from you. Specifically ${warningDescription}. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.

      -

      Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.

      -

      The Admin memember who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}

      -

      With Gratitude,

      -

      One Community

      `; - - if (sendEmail === '3') { - emailSender('arevaloluis114@gmail.com', 'Third Warning', emailTemplate, null, null); - } else { - emailSender('arevaloluis114@gmail.com', 'Fourth Warning', emailTemplate, null, null); +

      This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.

      +

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention and have resulted in a blue square being issued.

      +

      Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, feel free to ask questions—the Admin team is here to help.

      +

      The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them via email if you have any questions.

      +

      With Gratitude,

      +

      One Community

      `; + + if (sendEmail === 'issue warning') { + emailSender('arevaloluis114@gmail.com', subjectTitle, emailTemplate, null, null); + } else if (sendEmail === 'issue blue square') { + emailSender('arevaloluis114@gmail.com', subjectTitle, emailTemplate, null, null); } - - // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.thirdWarning.body}`; - // emailSender( - // 'arevaloluis114@gmail.com', - // emailTemplate.thirdWarning.subject, - // emailBody, - // null, - // null, - // ); }; -// gests the dsecriptions key from the array +// gets the dsecriptions key from the array const getDescriptionKey = (val) => // currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); @@ -192,15 +189,11 @@ const sortByColorAndDate = (a, b) => { return colorComparison; }; -const filterWarnings = ( - currentWarningDescriptions, - warnings, - description = null, - iconId = null, -) => { +const filterWarnings = (currentWarningDescriptions, warnings, iconId = null, color = null) => { const warningsObject = {}; let sendEmail = null; + let size = null; warnings.forEach((warning) => { if (!warningsObject[warning.description]) { @@ -209,32 +202,15 @@ const filterWarnings = ( warningsObject[warning.description].push(warning); if ( - warningsObject[warning.description].length === 3 && - description === warning.description && - sendEmail === null - ) { - sendEmail = '3'; - //send email - // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.thirdWarning.body}`; - // emailSender( - // 'arevaloluis114@gmail.com', - // emailTemplate.thirdWarning.subject, - // emailBody, - // null, - // null, - // ); - } else if ( - warningsObject[warning.description].length === 4 && - description === warning.description && - sendEmail === null + warningsObject[warning.description].length >= 3 && + warning.iconId === iconId && + color === 'yellow' ) { - sendEmail = '4'; - //send email - // const emailBody = `

      Hello ${currentUserName},

      \n ${emailTemplate.fourthWarning.body}`; - // emailSender( - // ' - } else { - sendEmail = null; + sendEmail = 'issue warning'; + size = warningsObject[warning.description].length; + } else if (warning.iconId === iconId && color === 'red') { + sendEmail = 'issue blue square'; + size = warningsObject[warning.description].length; } }); @@ -257,7 +233,7 @@ const filterWarnings = ( warnings: warns[descrip] ? warns[descrip] : [], }); } - return { completedData, sendEmail }; + return { completedData, sendEmail, size }; }; module.exports = warningsController; diff --git a/src/models/userProfile.js b/src/models/userProfile.js index e6e9e51aa..4f452cd52 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -127,6 +127,7 @@ const userProfileSchema = new Schema({ required: true, default: 'white', }, + iconId: { type: String, required: true }, }, ], location: { From 05a7524b442e6d477dcb52a3e4f1b94a46c4199c Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 19 Sep 2024 16:49:36 -0400 Subject: [PATCH 15/48] adjusted email text and body --- src/controllers/warningsController.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 8ad00ab8f..e45a25330 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -148,17 +148,23 @@ const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitor

      With Gratitude,

      One Community

      ` : `

      Hello ${currentUserName},

      -

      This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.

      -

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention and have resulted in a blue square being issued.

      +

      A blue square has been issued because this is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.

      +

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention, will result in additional blue square being issued, and could lead to termination.

      Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, feel free to ask questions—the Admin team is here to help.

      -

      The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them via email if you have any questions.

      +

      The Admin member who issued this blue square is ${monitorData.firstName} ${monitorData.lastName} and can be reached at ${monitorData.email}. If you have any questions, please comment on your Google Doc and tag them via email.

      With Gratitude,

      One Community

      `; if (sendEmail === 'issue warning') { emailSender('arevaloluis114@gmail.com', subjectTitle, emailTemplate, null, null); } else if (sendEmail === 'issue blue square') { - emailSender('arevaloluis114@gmail.com', subjectTitle, emailTemplate, null, null); + emailSender( + 'arevaloluis114@gmail.com', + `Blue Square issued for ${warningDescription}`, + emailTemplate, + null, + null, + ); } }; From d998541d5804ae173704f42b7230be86e06d7297 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Fri, 27 Sep 2024 19:52:31 -0700 Subject: [PATCH 16/48] adjusted email text when issuing a warning and blue square --- src/controllers/warningsController.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index e45a25330..ac64b0274 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -143,15 +143,15 @@ const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitor sendEmail === 'issue warning' ? `

      Hello ${currentUserName},

      This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}. Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, don’t hesitate to ask questions—the Admin team is here to assist.

      -

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention and may result in a blue square if it happens again.

      -

      The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them via email if you have any questions.

      +

      Moving forward, please ensure this issue is resolved. Repeated requests for the same thing require unnecessary administrative attention and may result in a blue square being issued if it happens again.

      +

      The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them using this email if you have any questions.

      With Gratitude,

      One Community

      ` : `

      Hello ${currentUserName},

      A blue square has been issued because this is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.

      -

      Please ensure this issue is resolved moving forward. Repeated requests for the same thing require unnecessary administrative attention, will result in additional blue square being issued, and could lead to termination.

      +

      Moving forward, please ensure this is resolved. Repeated requests for the same thing require unnecessary administrative attention, will result in an additional blue square being issued, and could lead to termination.

      Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, feel free to ask questions—the Admin team is here to help.

      -

      The Admin member who issued this blue square is ${monitorData.firstName} ${monitorData.lastName} and can be reached at ${monitorData.email}. If you have any questions, please comment on your Google Doc and tag them via email.

      +

      The Admin member who issued this blue square is ${monitorData.firstName} ${monitorData.lastName} and can be reached at ${monitorData.email}. If you have any questions, please comment on your Google Doc and tag them using this email.

      With Gratitude,

      One Community

      `; From 92b2ec13175b6302ed8786d77632c0f514ef159c Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Tue, 1 Oct 2024 20:49:20 -0700 Subject: [PATCH 17/48] fetch admin's of the team and send an email cc them when a warning and blue square is issued --- src/controllers/warningsController.js | 46 ++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index ac64b0274..16130196a 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const currentWarnings = require('../models/currentWarnings'); const emailSender = require('../utilities/emailSender'); - +const userHelper = require('../helpers/userHelper')(); let currentWarningDescriptions = null; let currentUserName = null; const emailTemplate = { @@ -71,7 +71,7 @@ const warningsController = function (UserProfile) { } currentUserName = `${record.firstName} ${record.lastName}`; - //check warning id + const updatedWarnings = await UserProfile.findByIdAndUpdate( { _id: userId, @@ -87,8 +87,9 @@ const warningsController = function (UserProfile) { color, ); + const adminEmails = await getUserRoleByEmail(record); if (sendEmail !== null) { - sendEmailToUser(sendEmail, description, currentUserName, monitorData, size); + sendEmailToUser(sendEmail, description, currentUserName, monitorData, size, adminEmails); } res.status(201).send({ message: 'success', warnings: completedData }); @@ -127,15 +128,34 @@ const warningsController = function (UserProfile) { }; }; +//helper to get the team members admin emails +async function getUserRoleByEmail(user) { + const recipients = []; + for (const teamId of user.teams) { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + } + return recipients; +} + //helper function to get the ordinal function getOrdinal(n) { const suffixes = ['th', 'st', 'nd', 'rd']; const value = n % 100; return n + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]); } -const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitorData, size) => { - //issued blue square? if so, send second tempalte - // +const sendEmailToUser = ( + sendEmail, + warningDescription, + currentUserName, + monitorData, + size, + adminEmails, +) => { const ordinal = getOrdinal(size); const subjectTitle = ordinal + ' Warning'; @@ -156,23 +176,27 @@ const sendEmailToUser = (sendEmail, warningDescription, currentUserName, monitor

      One Community

      `; if (sendEmail === 'issue warning') { - emailSender('arevaloluis114@gmail.com', subjectTitle, emailTemplate, null, null); + emailSender( + 'arevaloluis114@gmail.com', + subjectTitle, + emailTemplate, + adminEmails.toString(), + null, + ); } else if (sendEmail === 'issue blue square') { emailSender( 'arevaloluis114@gmail.com', `Blue Square issued for ${warningDescription}`, emailTemplate, - null, + adminEmails.toString(), null, ); } }; // gets the dsecriptions key from the array -const getDescriptionKey = (val) => - // currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); +const getDescriptionKey = (val) => currentWarningDescriptions.indexOf(val); - currentWarningDescriptions.indexOf(val); const sortKeysAlphabetically = (a, b) => getDescriptionKey(a) - getDescriptionKey(b); // method to see which color is first From 2ab3c4bb5959e141f3fce5e724d4a3e6aa49381a Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Tue, 1 Oct 2024 20:55:12 -0700 Subject: [PATCH 18/48] removed test code --- src/cronjobs/userProfileJobs.js | 4 ++-- src/helpers/userHelper.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index 8acd192f3..68af6a12e 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -5,8 +5,8 @@ const userhelper = require('../helpers/userHelper')(); const userProfileJobs = () => { const allUserProfileJobs = new CronJob( - '* * * * *', // Comment out for testing. Run Every minute. - // '1 0 * * 0', // Every Sunday, 1 minute past midnight. + // '* * * * *', // Comment out for testing. Run Every minute. + '1 0 * * 0', // Every Sunday, 1 minute past midnight. async () => { const SUNDAY = 0; // will change back to 0 after fix diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 59dd94fd8..7691be296 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -452,7 +452,7 @@ const userHelper = function () { const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); const users = await userProfile.find( - { isActive: true, email: 'arevaloluis114@gmail.com' }, + { isActive: true }, '_id weeklycommittedHours weeklySummaries missedHours', ); const usersRequiringBlueSqNotification = []; From 73d14f95a8ac1c6720698c801f12d9d452ac50f5 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 3 Oct 2024 21:54:23 -0700 Subject: [PATCH 19/48] updated the reciever of the email, instead of my email and adjust the user data by creating an object --- src/controllers/warningsController.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 16130196a..b68fc2968 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -70,7 +70,11 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'No valid records found' }); } - currentUserName = `${record.firstName} ${record.lastName}`; + const userAssignedWarning = { + firstName: record.firstName, + lastName: record.lastName, + email: record.email, + }; const updatedWarnings = await UserProfile.findByIdAndUpdate( { @@ -89,7 +93,14 @@ const warningsController = function (UserProfile) { const adminEmails = await getUserRoleByEmail(record); if (sendEmail !== null) { - sendEmailToUser(sendEmail, description, currentUserName, monitorData, size, adminEmails); + sendEmailToUser( + sendEmail, + description, + userAssignedWarning, + monitorData, + size, + adminEmails, + ); } res.status(201).send({ message: 'success', warnings: completedData }); @@ -151,7 +162,7 @@ function getOrdinal(n) { const sendEmailToUser = ( sendEmail, warningDescription, - currentUserName, + userAssignedWarning, monitorData, size, adminEmails, @@ -159,6 +170,7 @@ const sendEmailToUser = ( const ordinal = getOrdinal(size); const subjectTitle = ordinal + ' Warning'; + const currentUserName = `${userAssignedWarning.firstName} ${userAssignedWarning.lastName}`; const emailTemplate = sendEmail === 'issue warning' ? `

      Hello ${currentUserName},

      @@ -177,7 +189,7 @@ const sendEmailToUser = ( if (sendEmail === 'issue warning') { emailSender( - 'arevaloluis114@gmail.com', + `${userAssignedWarning.email}`, subjectTitle, emailTemplate, adminEmails.toString(), @@ -185,7 +197,7 @@ const sendEmailToUser = ( ); } else if (sendEmail === 'issue blue square') { emailSender( - 'arevaloluis114@gmail.com', + `${userAssignedWarning.email}`, `Blue Square issued for ${warningDescription}`, emailTemplate, adminEmails.toString(), From f74c289fb11ca0ef53ce78bc91e83c3a1eafa944 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Fri, 4 Oct 2024 19:48:59 -0700 Subject: [PATCH 20/48] removed duplicate admin emails --- src/controllers/warningsController.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index b68fc2968..1ea979b7e 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -105,7 +105,6 @@ const warningsController = function (UserProfile) { res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { - console.log('error', error); res.status(400).send({ message: error.message || error }); } }; @@ -150,7 +149,10 @@ async function getUserRoleByEmail(user) { }); } } - return recipients; + + const sortedList = [...new Set(recipients)]; + console.log(sortedList); + return [...new Set(recipients)]; } //helper function to get the ordinal From af2df1cfd03fce7730c5793c99fb7e2760cde72f Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Fri, 4 Oct 2024 20:40:36 -0700 Subject: [PATCH 21/48] removed log --- src/controllers/warningsController.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 1ea979b7e..8b4759355 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -150,8 +150,6 @@ async function getUserRoleByEmail(user) { } } - const sortedList = [...new Set(recipients)]; - console.log(sortedList); return [...new Set(recipients)]; } From 770086b5c43c616638f01c08d524ba683cbe3812 Mon Sep 17 00:00:00 2001 From: SFA23SCM35V Date: Thu, 10 Oct 2024 11:44:48 -0500 Subject: [PATCH 22/48] Bluesquare --- src/controllers/userProfileController.js | 97 +++++++++++++++++------- 1 file changed, 71 insertions(+), 26 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 1debe68f3..f1bdccd66 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1686,37 +1686,82 @@ const userProfileController = function (UserProfile, Project) { }); }; - const deleteInfringements = async function (req, res) { - if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { - res.status(403).send('You are not authorized to delete blue square'); - return; - } - const { userId, blueSquareId } = req.params; - // console.log(userId, blueSquareId); + // const deleteInfringements = async function (req, res) { + // if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { + // res.status(403).send('You are not authorized to delete blue square'); + // return; + // } + // const { userId, blueSquareId } = req.params; + // // console.log(userId, blueSquareId); + + // UserProfile.findById(userId, async (err, record) => { + // if (err || !record) { + // res.status(404).send('No valid records found'); + // return; + // } + + // const originalinfringements = record?.infringements ?? []; + + // record.infringements = originalinfringements.filter( + // (infringement) => !infringement._id.equals(blueSquareId), + // ); + + // record + // .save() + // .then((results) => { + // userHelper.notifyInfringements(originalinfringements, results.infringements); + // res.status(200).json({ + // _id: record._id, + // }); + // }) + // .catch((error) => res.status(400).send(error)); + // }); + // }; - UserProfile.findById(userId, async (err, record) => { - if (err || !record) { - res.status(404).send('No valid records found'); - return; - } + +/* +Used async/await consistently, removing the callback approach from findById. +Simplified error handling by wrapping the entire block in a try/catch. +Replaced redundant return statements with direct res responses for permission and record checks. +Improved readability and structure by removing unnecessary comments and logs. - const originalinfringements = record?.infringements ?? []; +*/ - record.infringements = originalinfringements.filter( - (infringement) => !infringement._id.equals(blueSquareId), + const deleteInfringements = async (req, res) => { + try { + // Check permissions + if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { + return res.status(403).send('You are not authorized to delete blue square'); + } + + const { userId, blueSquareId } = req.params; + const record = await UserProfile.findById(userId).exec(); + + // Check if the record exists + if (!record) { + return res.status(404).send('No valid records found'); + } + + const originalInfringements = record.infringements || []; + + // Filter out the infringement to be deleted + record.infringements = originalInfringements.filter( + (infringement) => !infringement._id.equals(blueSquareId) ); - - record - .save() - .then((results) => { - userHelper.notifyInfringements(originalinfringements, results.infringements); - res.status(200).json({ - _id: record._id, - }); - }) - .catch((error) => res.status(400).send(error)); - }); + + // Save the updated record + const updatedRecord = await record.save(); + + // Notify about the updated infringements + userHelper.notifyInfringements(originalInfringements, updatedRecord.infringements); + + // Respond with success + res.status(200).json({ _id: updatedRecord._id }); + } catch (error) { + res.status(400).send(error.message); + } }; + const getProjectsByPerson = async function (req, res) { try { From 455c37937620edad5b4e78756b93d4c78d1c4afa Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 10 Oct 2024 19:55:30 -0700 Subject: [PATCH 23/48] added jae's email --- src/controllers/warningsController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 8b4759355..6334fdb52 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -140,7 +140,7 @@ const warningsController = function (UserProfile) { //helper to get the team members admin emails async function getUserRoleByEmail(user) { - const recipients = []; + const recipients = ['onecommunityglobal@gmail.com']; for (const teamId of user.teams) { const managementEmails = await userHelper.getTeamManagementEmail(teamId); if (Array.isArray(managementEmails) && managementEmails.length > 0) { From 773186df765a7a82a1655cd3b7a271578e307f3e Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Thu, 10 Oct 2024 20:43:26 -0700 Subject: [PATCH 24/48] removed jaes email and replaced it with a dummy one --- src/controllers/warningsController.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 6334fdb52..fb1aa3eef 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -140,7 +140,8 @@ const warningsController = function (UserProfile) { //helper to get the team members admin emails async function getUserRoleByEmail(user) { - const recipients = ['onecommunityglobal@gmail.com']; + //replacement for jae's email + const recipients = ['test@test.com']; for (const teamId of user.teams) { const managementEmails = await userHelper.getTeamManagementEmail(teamId); if (Array.isArray(managementEmails) && managementEmails.length > 0) { From fe366748fae4862456e16cf7ff25ae5c3ea9dba5 Mon Sep 17 00:00:00 2001 From: SFA23SCM35V Date: Sun, 20 Oct 2024 04:17:27 -0500 Subject: [PATCH 25/48] removed comments --- src/controllers/userProfileController.js | 40 ------------------------ 1 file changed, 40 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index f1bdccd66..941665dec 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1686,46 +1686,6 @@ const userProfileController = function (UserProfile, Project) { }); }; - // const deleteInfringements = async function (req, res) { - // if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { - // res.status(403).send('You are not authorized to delete blue square'); - // return; - // } - // const { userId, blueSquareId } = req.params; - // // console.log(userId, blueSquareId); - - // UserProfile.findById(userId, async (err, record) => { - // if (err || !record) { - // res.status(404).send('No valid records found'); - // return; - // } - - // const originalinfringements = record?.infringements ?? []; - - // record.infringements = originalinfringements.filter( - // (infringement) => !infringement._id.equals(blueSquareId), - // ); - - // record - // .save() - // .then((results) => { - // userHelper.notifyInfringements(originalinfringements, results.infringements); - // res.status(200).json({ - // _id: record._id, - // }); - // }) - // .catch((error) => res.status(400).send(error)); - // }); - // }; - - -/* -Used async/await consistently, removing the callback approach from findById. -Simplified error handling by wrapping the entire block in a try/catch. -Replaced redundant return statements with direct res responses for permission and record checks. -Improved readability and structure by removing unnecessary comments and logs. - -*/ const deleteInfringements = async (req, res) => { try { From d66fd7f4e8c1a2956134edc82b81ae1cd67a8900 Mon Sep 17 00:00:00 2001 From: Ashish4n8 Date: Wed, 13 Nov 2024 16:26:20 -0600 Subject: [PATCH 26/48] updated canRequestorUpdateUser function to convert _id to string --- src/utilities/permissions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 2299e8812..75aee3b59 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -64,10 +64,10 @@ const canRequestorUpdateUser = async (requestorId, targetUserId) => { // 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); + .map(({ _id }) => _id.toString()); protectedEmailAccountIds = query .filter(({ email }) => PROTECTED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); + .map(({ _id }) => _id.toString()); serverCache.setCache('protectedEmailAccountIds', protectedEmailAccountIds); serverCache.setCache('allowedEmailAccountIds', allowedEmailAccountIds); From 9088646679c71f304fb11e084df4fec61a115279 Mon Sep 17 00:00:00 2001 From: One Community Date: Thu, 14 Nov 2024 18:17:43 -0800 Subject: [PATCH 27/48] 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 28/48] 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 cc702f19a754c2540025c65242f3f929b0630e21 Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:28:05 -0500 Subject: [PATCH 29/48] fix: user profile problems caused by #1128 --- src/controllers/userProfileController.js | 313 +++++++++++++++++++++-- src/routes/userProfileRouter.js | 14 + 2 files changed, 305 insertions(+), 22 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 8226aee65..f8b35ed02 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -196,6 +196,38 @@ const userProfileController = function (UserProfile, Project) { .catch((error) => res.status(404).send(error)); }; + /** + * Controller function to retrieve basic user profile information. + * This endpoint checks if the user has the necessary permissions to access user profiles. + * If authorized, it queries the database to fetch only the required fields: + * _id, firstName, lastName, isActive, startDate, and endDate, sorted by last name. + */ + const getUserProfileBasicInfo = async function (req, res) { + if (!(await checkPermission(req, 'getUserProfiles'))) { + forbidden(res, 'You are not authorized to view all users'); + return; + } + + await UserProfile.find({}, '_id firstName lastName isActive startDate createdDate endDate') + .sort({ + lastName: 1, + }) + .then((results) => { + if (!results) { + if (cache.getCache('allusers')) { + const getData = JSON.parse(cache.getCache('allusers')); + res.status(200).send(getData); + return; + } + res.status(500).send({ error: 'User result was invalid' }); + return; + } + cache.setCache('allusers', JSON.stringify(results)); + res.status(200).send(results); + }) + .catch((error) => res.status(404).send(error)); + }; + const getProjectMembers = async function (req, res) { if (!(await hasPermission(req.body.requestor, 'getProjectMembers'))) { res.status(403).send('You are not authorized to view all users'); @@ -326,6 +358,7 @@ const userProfileController = function (UserProfile, Project) { up.adminLinks = req.body.adminLinks; up.teams = Array.from(new Set(req.body.teams)); up.projects = Array.from(new Set(req.body.projects)); + up.teamCode = req.body.teamCode; up.createdDate = req.body.createdDate; up.startDate = req.body.startDate ? req.body.startDate : req.body.createdDate; up.email = req.body.email; @@ -505,7 +538,7 @@ const userProfileController = function (UserProfile, Project) { } }); - // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), + // 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 @@ -644,7 +677,7 @@ const userProfileController = function (UserProfile, Project) { } if (req.body.startDate !== undefined && record.startDate !== req.body.startDate) { - record.startDate = moment(req.body.startDate).toDate(); + record.startDate = moment.tz(req.body.startDate, 'America/Los_Angeles').toDate(); // Make sure weeklycommittedHoursHistory isn't empty if (record.weeklycommittedHoursHistory.length === 0) { const newEntry = { @@ -667,7 +700,7 @@ const userProfileController = function (UserProfile, Project) { if (req.body.endDate !== undefined) { if (yearMonthDayDateValidator(req.body.endDate)) { - record.endDate = moment(req.body.endDate).toDate(); + record.endDate = moment.tz(req.body.endDate, 'America/Los_Angeles').toDate(); if (isUserInCache) { userData.endDate = record.endDate.toISOString(); } @@ -684,12 +717,7 @@ const userProfileController = function (UserProfile, Project) { userData.startDate = record.startDate.toISOString(); } } - if ( - req.body.infringements !== undefined && - (await hasPermission(req.body.requestor, 'infringementAuthorizer')) - ) { - record.infringements = req.body.infringements; - } + let updatedDiff = null; if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { updatedDiff = record.modifiedPaths(); @@ -727,7 +755,17 @@ const userProfileController = function (UserProfile, Project) { 'update', ); }) - .catch((error) => res.status(400).send(error)); + .catch((error) => { + if (error.name === 'ValidationError' && error.errors.lastName) { + const errors = Object.values(error.errors).map((er) => er.message); + return res.status(400).json({ + message: 'Validation Error', + error: errors, + }); + } + console.error('Failed to save record:', error); + return res.status(400).json({ error: 'Failed to save record.' }); + }); }); }; @@ -843,11 +881,11 @@ const userProfileController = function (UserProfile, Project) { const getUserById = function (req, res) { const userid = req.params.userId; - if (cache.getCache(`user-${userid}`)) { - const getData = JSON.parse(cache.getCache(`user-${userid}`)); - res.status(200).send(getData); - return; - } + // if (cache.getCache(`user-${userid}`)) { + // const getData = JSON.parse(cache.getCache(`user-${userid}`)); + // res.status(200).send(getData); + // return; + // } UserProfile.findById(userid, '-password -refreshTokens -lastModifiedDate -__v') .populate([ @@ -877,6 +915,15 @@ const userProfileController = function (UserProfile, Project) { select: '_id badgeName type imageUrl description ranking showReport', }, }, + { + path: 'infringements', // Populate infringements field + select: 'date description', + options: { + sort: { + date: -1, // Sort by date descending if needed + }, + }, + }, ]) .exec() .then((results) => { @@ -1021,7 +1068,7 @@ const userProfileController = function (UserProfile, Project) { const hasUpdatePasswordPermission = await hasPermission(requestor, 'updatePassword'); // if they're updating someone else's password, they need the 'updatePassword' permission. - if (!hasUpdatePasswordPermission) { + if (userId !== requestor.requestorId && !hasUpdatePasswordPermission) { return res.status(403).send({ error: "You are unauthorized to update this user's password", }); @@ -1156,7 +1203,18 @@ const userProfileController = function (UserProfile, Project) { const activationDate = req.body.reactivationDate; const { endDate } = req.body; const isSet = req.body.isSet === 'FinalDay'; - + let activeStatus = status; + let emailThreeWeeksSent = false; + if (endDate && status) { + const dateObject = new Date(endDate); + dateObject.setHours(dateObject.getHours() + 7); + const setEndDate = dateObject; + if (moment().isAfter(moment(setEndDate).add(1, 'days'))) { + activeStatus = false; + } else if (moment().isBefore(moment(endDate).subtract(3, 'weeks'))) { + emailThreeWeeksSent = true; + } + } if (!mongoose.Types.ObjectId.isValid(userId)) { res.status(400).send({ error: 'Bad Request', @@ -1202,13 +1260,14 @@ const userProfileController = function (UserProfile, Project) { logger.logException(err, 'Unexpected error in finding menagement team'); } - UserProfile.findById(userId, 'isActive email firstName lastName') + UserProfile.findById(userId, 'isActive email firstName lastName finalEmailThreeWeeksSent') .then((user) => { user.set({ - isActive: status, + isActive: activeStatus, reactivationDate: activationDate, endDate, isSet, + finalEmailThreeWeeksSent: emailThreeWeeksSent, }); user .save() @@ -1232,6 +1291,8 @@ const userProfileController = function (UserProfile, Project) { user.email, recipients, isSet, + activationDate, + emailThreeWeeksSent, ); auditIfProtectedAccountUpdated( req.body.requestor.requestorId, @@ -1539,6 +1600,155 @@ const userProfileController = function (UserProfile, Project) { } }; + const addInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'addInfringements'))) { + res.status(403).send('You are not authorized to add blue square'); + return; + } + const userid = req.params.userId; + + cache.removeCache(`user-${userid}`); + + if (req.body.blueSquare === undefined) { + res.status(400).send('Invalid Data'); + return; + } + + UserProfile.findById(userid, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + // find userData in cache + const isUserInCache = cache.hasCache('allusers'); + let allUserData; + let userData; + let userIdx; + if (isUserInCache) { + allUserData = JSON.parse(cache.getCache('allusers')); + userIdx = allUserData.findIndex((users) => users._id === userid); + userData = allUserData[userIdx]; + } + + const originalinfringements = record?.infringements ?? []; + record.infringements = originalinfringements.concat(req.body.blueSquare); + + record + .save() + .then((results) => { + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); + res.status(200).json({ + _id: record._id, + }); + + // update alluser cache if we have cache + if (isUserInCache) { + allUserData.splice(userIdx, 1, userData); + cache.setCache('allusers', JSON.stringify(allUserData)); + } + }) + .catch((error) => res.status(400).send(error)); + }); + }; + + const editInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'editInfringements'))) { + res.status(403).send('You are not authorized to edit blue square'); + return; + } + const { userId, blueSquareId } = req.params; + const { dateStamp, summary } = req.body; + + UserProfile.findById(userId, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + + const originalinfringements = record?.infringements ?? []; + + record.infringements = originalinfringements.map((blueSquare) => { + if (blueSquare._id.equals(blueSquareId)) { + blueSquare.date = dateStamp ?? blueSquare.date; + blueSquare.description = summary ?? blueSquare.description; + } + return blueSquare; + }); + + record + .save() + .then((results) => { + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); + res.status(200).json({ + _id: record._id, + }); + }) + .catch((error) => res.status(400).send(error)); + }); + }; + + const deleteInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { + res.status(403).send('You are not authorized to delete blue square'); + return; + } + const { userId, blueSquareId } = req.params; + // console.log(userId, blueSquareId); + + UserProfile.findById(userId, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + + const originalinfringements = record?.infringements ?? []; + + record.infringements = originalinfringements.filter( + (infringement) => !infringement._id.equals(blueSquareId), + ); + + record + .save() + .then((results) => { + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); + res.status(200).json({ + _id: record._id, + }); + }) + .catch((error) => res.status(400).send(error)); + }); + }; + const getProjectsByPerson = async function (req, res) { try { const { name } = req.params; @@ -1597,21 +1807,74 @@ const userProfileController = function (UserProfile, Project) { return teamCodes; } const distinctTeamCodes = await UserProfile.distinct('teamCode', { - teamCode: { $ne: null } + 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 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!' }); + return res + .status(500) + .send({ message: 'Encountered an error to get all team codes, please try again!' }); + } + }; + + const getUserByAutocomplete = (req, res) => { + const { searchText } = req.params; + + if (!searchText) { + return res.status(400).send({ message: 'Search text is required' }); + } + + const regex = new RegExp(searchText, 'i'); // Case-insensitive regex for partial matching + + UserProfile.find( + { + $or: [ + { firstName: { $regex: regex } }, + { lastName: { $regex: regex } }, + { + $expr: { + $regexMatch: { + input: { $concat: ['$firstName', ' ', '$lastName'] }, + regex: searchText, + options: 'i', + }, + }, + }, + ], + }, + '_id firstName lastName', // Projection to limit fields returned + ) + .limit(10) // Limit results for performance + .then((results) => { + res.status(200).send(results); + }) + .catch(() => { + res.status(500).send({ error: 'Internal Server Error' }); + }); + }; + + const updateUserInformation = async function (req,res){ + try { + const data=req.body; + data.map(async (e)=> { + const result = await UserProfile.findById(e.user_id); + result[e.item]=e.value + await result.save(); + }) + res.status(200).send({ message: 'Update successful'}); + } catch (error) { + console.log(error) + return res.status(500) } } @@ -1637,9 +1900,15 @@ const userProfileController = function (UserProfile, Project) { getUserByFullName, changeUserRehireableStatus, authorizeUser, + addInfringements, + editInfringements, + deleteInfringements, getProjectsByPerson, getAllTeamCode, getAllTeamCodeHelper, + getUserByAutocomplete, + getUserProfileBasicInfo, + updateUserInformation, }; }; diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index bf6f79237..2d68d2da1 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -23,6 +23,9 @@ const routes = function (userProfile, project) { controller.postUserProfile, ); + userProfileRouter.route('/userProfile/update').patch(controller.updateUserInformation); + // Endpoint to retrieve basic user profile information + userProfileRouter.route('/userProfile/basicInfo').get(controller.getUserProfileBasicInfo); userProfileRouter .route('/userProfile/:userId') .get(controller.getUserById) @@ -102,10 +105,21 @@ const routes = function (userProfile, project) { .route('/userProfile/authorizeUser/weeeklySummaries') .post(controller.authorizeUser); + userProfileRouter.route('/userProfile/:userId/addInfringement').post(controller.addInfringements); + + userProfileRouter + .route('/userProfile/:userId/infringements/:blueSquareId') + .put(controller.editInfringements) + .delete(controller.deleteInfringements); + userProfileRouter.route('/userProfile/projects/:name').get(controller.getProjectsByPerson); userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); + userProfileRouter + .route('/userProfile/autocomplete/:searchText') + .get(controller.getUserByAutocomplete); + return userProfileRouter; }; From 933161957622eca4dc4d550da19334234ce9546b Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Sat, 7 Dec 2024 16:08:04 -0500 Subject: [PATCH 30/48] fix: update title in title controller. --- src/controllers/titleController.js | 248 +++++++++++++++-------- src/controllers/userProfileController.js | 42 ++-- 2 files changed, 183 insertions(+), 107 deletions(-) diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 08751bdef..277842c43 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,130 +1,206 @@ -const Team = require('../models/team'); const Project = require('../models/project'); const cacheClosure = require('../utilities/nodeCache'); -const { getAllTeamCodeHelper } = require("./userProfileController"); +const userProfileController = require("./userProfileController"); +const userProfile = require('../models/userProfile'); +const project = require('../models/project'); + +const controller = userProfileController(userProfile, project); +const { getAllTeamCodeHelper } = controller; const titlecontroller = function (Title) { const cache = cacheClosure(); - const getAllTitles = function (req, res) { - Title.find({}) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).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); + } catch (error) { + console.error('Error checking if team code exists:', error); + throw error; + } + } + + async function checkProjectExists(projectID) { + try { + const proj = await Project.findOne({ _id: projectID }).exec(); + return !!proj; + } catch (error) { + console.error('Error checking if project exists:', error); + throw error; + } + } + + const getAllTitles = function (req, res) { + Title.find({}) + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); }; - const getTitleById = function (req, res) { - const { titleId } = req.params; + const getTitleById = function (req, res) { + const { titleId } = req.params; - Title.findById(titleId) - .then((results) => res.send(results)) - .catch((error) => res.send(error)); - }; + Title.findById(titleId) + .then((results) => res.send(results)) + .catch((error) => res.send(error)); + }; + + const postTitle = async function (req, res) { + const title = new Title(); + title.titleName = req.body.titleName; + title.titleCode = req.body.titleCode; + title.teamCode = req.body.teamCode; + title.projectAssigned = req.body.projectAssigned; + title.mediaFolder = req.body.mediaFolder; + title.teamAssiged = req.body.teamAssiged; + + const titleCodeRegex = /^[A-Za-z]+$/; + if (!title.titleCode || !title.titleCode.trim()) { + return res.status(400).send({ message: 'Title code cannot be empty.' }); + } - const postTitle = async function (req, res) { - const title = new Title(); + if (!titleCodeRegex.test(title.titleCode)) { + return res.status(400).send({ message: 'Title Code must contain only upper or lower case letters.' }); + } + + // valid title name + if (!title.titleName.trim()) { + res.status(400).send({ message: 'Title cannot be empty.' }); + return; + } - title.titleName = req.body.titleName; - title.teamCode = req.body.teamCode; - title.projectAssigned = req.body.projectAssigned; - title.mediaFolder = req.body.mediaFolder; - title.teamAssiged = req.body.teamAssiged; + // if media is empty + if (!title.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } + + if (!title.teamCode) { + res.status(400).send({ message: 'Please provide a team code.' }); + return; + } + + const teamCodeExists = await checkTeamCodeExists(title.teamCode); + if (!teamCodeExists) { + res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); + return; + } + + // validate if project exist + const projectExist = await checkProjectExists(title.projectAssigned._id); + if (!projectExist) { + res.status(400).send({ message: 'Project lalala is empty or not exist!!!' }); + return; + } + + // validate if team exist + if (title.teamAssiged && title.teamAssiged._id === 'N/A') { + res.status(400).send({ message: 'Team not exists.' }); + return; + } + + title + .save() + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)) + }; + + // update title function. + const updateTitle = async function (req, res) { + try { + + const filter = req.body.id; // valid title name - if (!title.titleName.trim()) { + if (!req.body.titleName.trim()) { res.status(400).send({ message: 'Title cannot be empty.' }); return; } - // if media is empty - if (!title.mediaFolder.trim()) { - res.status(400).send({ message: 'Media folder cannot be empty.' }); + if (!req.body.titleCode.trim()) { + res.status(400).send({ message: 'Title code cannot be empty.' }); return; } - const shortnames = title.titleName.trim().split(' '); - let shortname; - if (shortnames.length > 1) { - shortname = (shortnames[0][0] + shortnames[1][0]).toUpperCase(); - } else if (shortnames.length === 1) { - shortname = shortnames[0][0].toUpperCase(); + const titleCodeRegex = /^[A-Za-z]+$/; + if (!titleCodeRegex.test(req.body.titleCode)) { + return res.status(400).send({ message: 'Title Code must contain only upper or lower case letters.' }); } - title.shortName = shortname; - // Validate team code by checking if it exists in the database - if (!title.teamCode) { + // if media is empty + if (!req.body.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } + + if (!req.body.teamCode) { res.status(400).send({ message: 'Please provide a team code.' }); return; } - const teamCodeExists = await checkTeamCodeExists(title.teamCode); + const teamCodeExists = await checkTeamCodeExists(req.body.teamCode); if (!teamCodeExists) { res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); return; } // validate if project exist - const projectExist = await checkProjectExists(title.projectAssigned._id); + const projectExist = await checkProjectExists(req.body.projectAssigned._id); if (!projectExist) { - res.status(400).send({ message: 'Project is empty or not exist.' }); + res.status(400).send({ message: 'Project is empty or not exist~~~' }); return; } // validate if team exist - if (title.teamAssiged && title.teamAssiged._id === 'N/A') { + if (req.body.teamAssiged && req.body.teamAssiged._id === 'N/A') { res.status(400).send({ message: 'Team not exists.' }); return; } + const result = await Title.findById(filter); + result.titleName = req.body.titleName; + result.titleCode = req.body.titleCode; + result.teamCode = req.body.teamCode; + result.projectAssigned = req.body.projectAssigned; + result.mediaFolder = req.body.mediaFolder; + result.teamAssiged = req.body.teamAssiged; + const updatedTitle = await result.save(); + res.status(200).send({ message: 'Update successful', updatedTitle }); + + } catch (error) { + console.log(error); + res.status(500).send({ message: 'An error occurred', error }); + } + + }; + + const deleteTitleById = async function (req, res) { + const { titleId } = req.params; + Title.deleteOne({ _id: titleId }) + .then((result) => res.send(result)) + .catch((error) => res.send(error)); + }; - title - .save() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); - }; - - const deleteTitleById = async function (req, res) { - const { titleId } = req.params; - Title.deleteOne({ _id: titleId }) - .then((result) => res.send(result)) - .catch((error) => res.send(error)); - }; - - const deleteAllTitles = async function (req, res) { - Title.deleteMany({}) - .then((result) => { - if (result.deletedCount === 0) { - res.send({ message: 'No titles found to delete.' }); - } else { - res.send({ message: `${result.deletedCount} titles were deleted successfully.` }); - } - }) - .catch((error) => { - 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 deleteAllTitles = async function (req, res) { + Title.deleteMany({}) + .then((result) => { + if (result.deletedCount === 0) { + res.send({ message: 'No titles found to delete.' }); + } else { + res.send({ message: `${result.deletedCount} titles were deleted successfully.` }); } - const teamCodes = await getAllTeamCodeHelper(); - return teamCodes.includes(teamCode); - } catch (error) { - console.error('Error checking if team code exists:', error); - throw error; - } - } + }) + .catch((error) => { + console.log(error) + res.status(500).send(error); + }); + }; + + - async function checkProjectExists(projectID) { - try { - const project = await Project.findOne({ _id: projectID }).exec(); - return !!project; - } catch (error) { - console.error('Error checking if project exists:', error); - throw error; - } - } return { getAllTitles, @@ -132,8 +208,8 @@ const titlecontroller = function (Title) { postTitle, deleteTitleById, deleteAllTitles, + updateTitle }; }; - module.exports = titlecontroller; - \ No newline at end of file +module.exports = titlecontroller; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index f8b35ed02..83a21a4da 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1758,28 +1758,28 @@ const userProfileController = function (UserProfile, Project) { 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') }, + }, + { + $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') }, - }, - ], - }; + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + ], + }; const userProfile = await UserProfile.find(query); From 276798bbedf7931682f3f768868a81504a4a3573 Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Sat, 7 Dec 2024 16:11:14 -0500 Subject: [PATCH 31/48] fix: add updateTitle back in titleRouter.js. --- src/routes/titleRouter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js index f12cb5ec7..1bced1e08 100644 --- a/src/routes/titleRouter.js +++ b/src/routes/titleRouter.js @@ -2,12 +2,14 @@ const express = require('express'); const router = function (title) { const controller = require('../controllers/titleController')(title); - const titleRouter = express.Router(); titleRouter.route('/title') .get(controller.getAllTitles) - .post(controller.postTitle); + .post(controller.postTitle) + // .put(controller.putTitle); + + titleRouter.route('/title/update').post(controller.updateTitle); titleRouter.route('/title/:titleId') .get(controller.getTitleById) From c4dc723cd203442bdfeac63cc14d6e64ca1340cd Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:40:53 -0500 Subject: [PATCH 32/48] fix: revert some changes in #1128 --- src/controllers/timeEntryController.js | 189 ++++++++++++++--- src/helpers/dashboardhelper.js | 267 +------------------------ src/helpers/taskHelper.js | 8 +- src/helpers/userHelper.js | 204 ++++++++++++++----- src/models/team.js | 10 +- src/models/timeentry.js | 2 + src/models/title.js | 4 +- src/models/userProfile.js | 1 + src/routes/timeentryRouter.js | 12 +- 9 files changed, 345 insertions(+), 352 deletions(-) diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 6e8d36596..44a50fcfb 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1,5 +1,6 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); const logger = require('../startup/logger'); const UserProfile = require('../models/userProfile'); const Project = require('../models/project'); @@ -419,7 +420,7 @@ const addEditHistory = async (

      One Community

              -
      +

      ADMINISTRATIVE DETAILS:

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

      Role: ${userprofile.role}

      @@ -593,7 +594,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); } @@ -866,7 +867,7 @@ const timeEntrycontroller = function (TimeEntry) { } await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -939,7 +940,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.remove({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -1062,40 +1063,115 @@ const timeEntrycontroller = function (TimeEntry) { }); }; - const getTimeEntriesForReports = function (req, res) { + const getTimeEntriesForReports =async function (req, res) { const { users, fromDate, toDate } = req.body; + const cacheKey = `timeEntry_${fromDate}_${toDate}`; + const timeentryCache=cacheClosure(); + const cacheData=timeentryCache.hasCache(cacheKey) + if(cacheData){ + const data = timeentryCache.getCache(cacheKey); + return res.status(200).send(data); + } + try { + const results = await TimeEntry.find( + { + personId: { $in: users }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + '-createdDateTime' // Exclude unnecessary fields + ) + .lean() // Returns plain JavaScript objects, not Mongoose documents + .populate({ + path: 'projectId', + select: '_id projectName', // Only return necessary fields from the project + }) + .exec(); // Executes the query + const data = results.map(element => { + const record = { + _id: element._id, + isTangible: element.isTangible, + personId: element.personId, + dateOfWork: element.dateOfWork, + hours: formatSeconds(element.totalSeconds)[0], + minutes: formatSeconds(element.totalSeconds)[1], + projectId: element.projectId?._id || '', + projectName: element.projectId?.projectName || '', + }; + return record; + }); + timeentryCache.setCache(cacheKey,data); + return res.status(200).send(data); + } catch (error) { + res.status(400).send(error); + } + }; + const getTimeEntriesForProjectReports = function (req, res) { + const { users, fromDate, toDate } = req.body; + + // Fetch only necessary fields and avoid bringing the entire document TimeEntry.find( { personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, - ' -createdDateTime', + 'totalSeconds isTangible dateOfWork projectId', ) - .populate('projectId') - + .populate('projectId', 'projectName _id') + .lean() // lean() for better performance as we don't need Mongoose document methods .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; + const data = results.map((element) => { + const record = { + isTangible: element.isTangible, + dateOfWork: element.dateOfWork, + projectId: element.projectId ? element.projectId._id : '', + projectName: element.projectId ? element.projectId.projectName : '', + }; + + // Convert totalSeconds to hours and minutes [record.hours, record.minutes] = formatSeconds(element.totalSeconds); - record.projectId = element.projectId ? element.projectId._id : ''; - record.projectName = element.projectId ? element.projectId.projectName : ''; - data.push(record); + + return record; }); res.status(200).send(data); }) .catch((error) => { - res.status(400).send(error); + res.status(400).send({ message: 'Error fetching time entries for project reports', error }); }); }; + const getTimeEntriesForPeopleReports = async function (req, res) { + try { + const { users, fromDate, toDate } = req.body; + + const results = await TimeEntry.find( + { + personId: { $in: users }, + dateOfWork: { $gte: fromDate, $lte: toDate }, + }, + 'personId totalSeconds isTangible dateOfWork', + ).lean(); // Use lean() for better performance + + const data = results + .map((entry) => { + const [hours, minutes] = formatSeconds(entry.totalSeconds); + return { + personId: entry.personId, + hours, + minutes, + isTangible: entry.isTangible, + dateOfWork: entry.dateOfWork, + }; + }) + .filter(Boolean); + + res.status(200).send(data); + } catch (error) { + res.status(400).send({ message: 'Error fetching time entries for people reports', error }); + } + }; + /** * Get time entries for a specified project */ @@ -1208,7 +1284,12 @@ const timeEntrycontroller = function (TimeEntry) { */ const getLostTimeEntriesForTeamList = function (req, res) { const { teams, fromDate, toDate } = req.body; - + const lostteamentryCache=cacheClosure() + const cacheKey = `LostTeamEntry_${fromDate}_${toDate}`; + const cacheData=lostteamentryCache.getCache(cacheKey) + if(cacheData){ + return res.status(200).send(cacheData) + } TimeEntry.find( { entryType: 'team', @@ -1217,7 +1298,7 @@ const timeEntrycontroller = function (TimeEntry) { isActive: { $ne: false }, }, ' -createdDateTime', - ) + ).lean() .populate('teamId') .sort({ lastModifiedDateTime: -1 }) .then((results) => { @@ -1234,7 +1315,8 @@ const timeEntrycontroller = function (TimeEntry) { [record.hours, record.minutes] = formatSeconds(element.totalSeconds); data.push(record); }); - res.status(200).send(data); + lostteamentryCache.setCache(cacheKey,data); + return res.status(200).send(data); }) .catch((error) => { res.status(400).send(error); @@ -1367,10 +1449,12 @@ const timeEntrycontroller = function (TimeEntry) { return newTotalIntangibleHrs; }; + const recalculationTaskQueue = []; + /** - * recalculate the hoursByCatefory for all users and update the field + * recalculate the hoursByCategory for all users and update the field */ - const recalculateHoursByCategoryAllUsers = async function (req, res) { + const recalculateHoursByCategoryAllUsers = async function (taskId) { const session = await mongoose.startSession(); session.startTransaction(); @@ -1385,18 +1469,60 @@ const timeEntrycontroller = function (TimeEntry) { await Promise.all(recalculationPromises); await session.commitTransaction(); - return res.status(200).send({ - message: 'finished the recalculation for hoursByCategory for all users', - }); + + const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId); + if (recalculationTask) { + recalculationTask.status = 'Completed'; + recalculationTask.completionTime = new Date().toISOString(); + } } catch (err) { await session.abortTransaction(); + const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId); + if (recalculationTask) { + recalculationTask.status = 'Failed'; + recalculationTask.completionTime = new Date().toISOString(); + } + logger.logException(err); - return res.status(500).send({ error: err.toString() }); } finally { session.endSession(); } }; + const startRecalculation = async function (req, res) { + const taskId = uuidv4(); + recalculationTaskQueue.push({ + taskId, + status: 'In progress', + startTime: new Date().toISOString(), + completionTime: null, + }); + if (recalculationTaskQueue.length > 10) { + recalculationTaskQueue.shift(); + } + + res.status(200).send({ + message: 'The recalculation task started in the background', + taskId, + }); + + setTimeout(() => recalculateHoursByCategoryAllUsers(taskId), 0); + }; + + const checkRecalculationStatus = async function (req, res) { + const { taskId } = req.params; + const recalculationTask = recalculationTaskQueue.find((task) => task.taskId === taskId); + if (recalculationTask) { + res.status(200).send({ + status: recalculationTask.status, + startTime: recalculationTask.startTime, + completionTime: recalculationTask.completionTime, + }); + } else { + res.status(404).send({ message: 'Task not found' }); + } + }; + /** * recalculate the totalIntangibleHrs for all users and update the field */ @@ -1441,9 +1567,12 @@ const timeEntrycontroller = function (TimeEntry) { getLostTimeEntriesForTeamList, backupHoursByCategoryAllUsers, backupIntangibleHrsAllUsers, - recalculateHoursByCategoryAllUsers, recalculateIntangibleHrsAllUsers, getTimeEntriesForReports, + getTimeEntriesForProjectReports, + getTimeEntriesForPeopleReports, + startRecalculation, + checkRecalculationStatus, }; }; diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 533dbe367..dddb7cca2 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -181,14 +181,11 @@ const dashboardhelper = function () { _myTeam.members.forEach((teamMember) => { if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; }); - if(isUserVisible) - { + 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( @@ -203,8 +200,8 @@ const dashboardhelper = function () { timeOffFrom: 1, timeOffTill: 1, endDate: 1, - } - + missedHours: 1, + }, ); } else { // 'Core Team', 'Owner' //All users @@ -220,7 +217,7 @@ const dashboardhelper = function () { timeOffFrom: 1, timeOffTill: 1, endDate: 1, - + missedHours: 1, }, ); } @@ -269,6 +266,7 @@ const dashboardhelper = function () { ? teamMember.weeklySummaries[0].summary !== '' : false, weeklycommittedHours: teamMember.weeklycommittedHours, + missedHours: teamMember.missedHours ?? 0, totaltangibletime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds ?? 0) / 3600, totalintangibletime_hrs: @@ -309,255 +307,6 @@ const dashboardhelper = function () { console.log(error); return new Error(error); } - - // return myTeam.aggregate([ - // { - // $match: { - // _id: userid, - // }, - // }, - // { - // $unwind: '$myteam', - // }, - // { - // $project: { - // _id: 0, - // role: 1, - // personId: '$myteam._id', - // name: '$myteam.fullName', - // }, - // }, - // { - // $lookup: { - // from: 'userProfiles', - // localField: 'personId', - // foreignField: '_id', - // as: 'persondata', - // }, - // }, - // { - // $match: { - // // leaderboard user roles hierarchy - // $or: [ - // { - // role: { $in: ['Owner', 'Core Team'] }, - // }, - // { - // $and: [ - // { - // role: 'Administrator', - // }, - // { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, - // ], - // }, - // { - // $and: [ - // { - // role: { $in: ['Manager', 'Mentor'] }, - // }, - // { - // 'persondata.0.role': { - // $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], - // }, - // }, - // ], - // }, - // { 'persondata.0._id': userId }, - // { 'persondata.0.role': 'Volunteer' }, - // { 'persondata.0.isVisible': true }, - // ], - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: { - // $arrayElemAt: ['$persondata.role', 0], - // }, - // isVisible: { - // $arrayElemAt: ['$persondata.isVisible', 0], - // }, - // hasSummary: { - // $ne: [ - // { - // $arrayElemAt: [ - // { - // $arrayElemAt: ['$persondata.weeklySummaries.summary', 0], - // }, - // 0, - // ], - // }, - // '', - // ], - // }, - // weeklycommittedHours: { - // $sum: [ - // { - // $arrayElemAt: ['$persondata.weeklycommittedHours', 0], - // }, - // { - // $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], - // }, - // ], - // }, - // }, - // }, - // { - // $lookup: { - // from: 'timeEntries', - // localField: 'personId', - // foreignField: 'personId', - // as: 'timeEntryData', - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // timeEntryData: { - // $filter: { - // input: '$timeEntryData', - // as: 'timeentry', - // cond: { - // $and: [ - // { - // $gte: ['$$timeentry.dateOfWork', pdtstart], - // }, - // { - // $lte: ['$$timeentry.dateOfWork', pdtend], - // }, - // ], - // }, - // }, - // }, - // }, - // }, - // { - // $unwind: { - // path: '$timeEntryData', - // preserveNullAndEmptyArrays: true, - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // totalSeconds: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.totalSeconds', - // 0, - // ], - // }, - // isTangible: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.isTangible', - // false, - // ], - // }, - // }, - // }, - // { - // $addFields: { - // tangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', true], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // intangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', false], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // }, - // }, - // { - // $group: { - // _id: { - // personId: '$personId', - // weeklycommittedHours: '$weeklycommittedHours', - // name: '$name', - // role: '$role', - // isVisible: '$isVisible', - // hasSummary: '$hasSummary', - // }, - // totalSeconds: { - // $sum: '$totalSeconds', - // }, - // tangibletime: { - // $sum: '$tangibletime', - // }, - // intangibletime: { - // $sum: '$intangibletime', - // }, - // }, - // }, - // { - // $project: { - // _id: 0, - // personId: '$_id.personId', - // name: '$_id.name', - // role: '$_id.role', - // isVisible: '$_id.isVisible', - // hasSummary: '$_id.hasSummary', - // weeklycommittedHours: '$_id.weeklycommittedHours', - // totaltime_hrs: { - // $divide: ['$totalSeconds', 3600], - // }, - // totaltangibletime_hrs: { - // $divide: ['$tangibletime', 3600], - // }, - // totalintangibletime_hrs: { - // $divide: ['$intangibletime', 3600], - // }, - // percentagespentintangible: { - // $cond: [ - // { - // $eq: ['$totalSeconds', 0], - // }, - // 0, - // { - // $multiply: [ - // { - // $divide: ['$tangibletime', '$totalSeconds'], - // }, - // 100, - // ], - // }, - // ], - // }, - // }, - // }, - // { - // $sort: { - // totaltangibletime_hrs: -1, - // name: 1, - // role: 1, - // }, - // }, - // ]); }; /** diff --git a/src/helpers/taskHelper.js b/src/helpers/taskHelper.js index 34fb36be8..fefa9f021 100644 --- a/src/helpers/taskHelper.js +++ b/src/helpers/taskHelper.js @@ -112,9 +112,15 @@ const taskHelper = function () { ); sharedTeamsResult.forEach((_myTeam) => { + let hasTeamVisibility = false; _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + if (teamMember.userId.equals(userid) && teamMember.visible) hasTeamVisibility = true; }); + if (hasTeamVisibility) { + _myTeam.members.forEach((teamMember) => { + if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + }); + } }); teamMembers = await userProfile diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index ed9c52131..5195e8a37 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -146,14 +146,14 @@ const userHelper = function () { .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 + )}
      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. + )} 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 @@ -204,7 +204,7 @@ const userHelper = function () {

      One Community

              -
      +

      ADMINISTRATIVE DETAILS:

      Start Date: ${administrativeContent.startDate}

      Role: ${administrativeContent.role}

      @@ -656,7 +656,7 @@ const userHelper = function () { } // 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'); @@ -703,7 +703,7 @@ const userHelper = function () { .localeData() .ordinal( oldInfringements.length + 1, - )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( + )} blue square. So you should have completed ${weeklycommittedHours + coreTeamExtraHour} hours and you completed ${timeSpent.toFixed( 2, )} hours.`; } else { @@ -727,7 +727,7 @@ const userHelper = function () { .localeData() .ordinal( oldInfringements.length + 1, - )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( + )} blue square. So you should have completed ${weeklycommittedHours + coreTeamExtraHour} hours and you completed ${timeSpent.toFixed( 2, )} hours.`; } else { @@ -956,29 +956,54 @@ const userHelper = function () { $project: { _id: 1, missedHours: { - $max: [ - { - $subtract: [ - { - $sum: [{ $ifNull: ['$missedHours', 0] }, '$weeklycommittedHours'], - }, - { - $divide: [ - { - $sum: { - $map: { - input: '$timeEntries', - in: '$$this.totalSeconds', - }, + $let: { + vars: { + baseMissedHours: { + $max: [ + { + $subtract: [ + { + $sum: [{ $ifNull: ['$missedHours', 0] }, '$weeklycommittedHours'], }, - }, - 3600, - ], - }, + { + $divide: [ + { + $sum: { + $map: { + input: '$timeEntries', + in: '$$this.totalSeconds', + }, + }, + }, + 3600, + ], + }, + ], + }, + 0, + ], + }, + infringementsAdjustment: { + $cond: [ + { + $and: [ + { $gt: ['$infringements', null] }, + { $gt: [{ $size: '$infringements' }, 5] }, + ], + }, + { $subtract: [{ $size: '$infringements' }, 5] }, + 0, + ], + }, + }, + in: { + $cond: [ + { $gt: ['$$baseMissedHours', 0] }, + { $add: ['$$baseMissedHours', '$$infringementsAdjustment'] }, + '$$baseMissedHours', ], }, - 0, - ], + }, }, }, }, @@ -1024,7 +1049,7 @@ const userHelper = function () { }, ); - logger.logInfo(`Job deleting blue squares older than 1 year finished + 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); @@ -1074,11 +1099,11 @@ const userHelper = function () { const emailBody = `

      Hi Admin!

      This email is to let you know that ${person.firstName} ${person.lastName} has been made active again in the Highest Good Network application after being paused on ${endDate}.

      - +

      If you need to communicate anything with them, this is their email from the system: ${person.email}.

      - +

      Thanks!

      - +

      The HGN A.I. (and One Community)

      `; emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); @@ -2037,35 +2062,77 @@ const userHelper = function () { email, recipients, isSet, + reactivationDate, + sendThreeWeeks, + followup, ) { - if (endDate && !isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; - const emailBody = `

      Management,

      + let subject; + let emailBody; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); + if (reactivationDate) { + subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; + emailBody = `

      Management,

      + +

      Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

      +

      For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.

      -

      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); - } else if (isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; + emailSender(email, subject, emailBody, null, recipients, email); + } else if (endDate && isSet && sendThreeWeeks) { + const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `

      Management,

      -

      Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network ${endDate}. - For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

      - +

      Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

      +

      This is more than 3 weeks from now, but you should still start confirming all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

      + +

      An additional reminder email will be sent in their final 2 weeks.

      + +

      With Gratitude,

      + +

      One Community

      `; + emailSender(email, subject, emailBody, null, recipients, email); + + } else if (endDate && isSet && followup) { + subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + emailBody = `

      Management,

      + +

      Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

      +

      This is coming up soon. For a smooth transition, please confirm all your work is wrapped up with this individual and nothing further will be needed on their part after this date.

      +

      With Gratitude,

      - +

      One Community

      `; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); + emailSender(email, subject, emailBody, null, recipients, email); + + } else if (endDate && isSet ) { + subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + emailBody = `

      Management,

      + +

      Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

      +

      For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

      + +

      With Gratitude,

      + +

      One Community

      `; + emailSender(email, subject, emailBody, null, recipients, email); + + } else if(endDate){ + subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; + emailBody = `

      Management,

      + +

      Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

      +

      For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

      + +

      With Gratitude,

      + +

      One Community

      `; + emailSender(email, subject, emailBody, null, recipients, email); + }; + } - }; const deActivateUser = async () => { try { @@ -2076,13 +2143,38 @@ const userHelper = function () { const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, - '_id isActive endDate isSet', + '_id isActive endDate isSet finalEmailThreeWeeksSent reactivationDate', ); for (let i = 0; i < users.length; i += 1) { const user = users[i]; - const { endDate } = user; + const { endDate, finalEmailThreeWeeksSent } = user; endDate.setHours(endDate.getHours() + 7); - if (moment().isAfter(moment(endDate).add(1, 'days'))) { + // notify reminder set final day before 2 weeks + if(finalEmailThreeWeeksSent && moment().isBefore(moment(endDate).subtract(2, 'weeks')) && moment().isAfter(moment(endDate).subtract(3, 'weeks'))){ + 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}'s final Day is set 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, + person.isSet, + person.reactivationDate, + false, + true, + ); + } else if (moment().isAfter(moment(endDate).add(1, 'days'))) { try { await userProfile.findByIdAndUpdate( user._id, @@ -2115,6 +2207,8 @@ const userHelper = function () { person.email, recipients, person.isSet, + person.reactivationDate, + undefined, ); } } diff --git a/src/models/team.js b/src/models/team.js index 4d73615f5..109d93221 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -3,9 +3,9 @@ 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. + * 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({ @@ -15,9 +15,10 @@ const team = new Schema({ modifiedDatetime: { type: Date, default: Date.now() }, members: [ { - userId: { type: mongoose.SchemaTypes.ObjectId, required: true }, + userId: { type: mongoose.SchemaTypes.ObjectId, required: true, index : true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, visible: { type : 'Boolean', default:true}, + }, ], // Deprecated field @@ -35,4 +36,5 @@ const team = new Schema({ }, }); + module.exports = mongoose.model('team', team, 'teams'); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index ea5303b3a..1535ab13e 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -17,5 +17,7 @@ const TimeEntry = new Schema({ lastModifiedDateTime: { type: Date, default: Date.now }, isActive: { type: Boolean, default: true }, }); +TimeEntry.index({ personId: 1, dateOfWork: 1 }); +TimeEntry.index({ entryType: 1, teamId: 1, dateOfWork: 1, isActive: 1 }); module.exports = mongoose.model('timeEntry', TimeEntry, 'timeEntries'); diff --git a/src/models/title.js b/src/models/title.js index 64b9aed92..a41063aea 100644 --- a/src/models/title.js +++ b/src/models/title.js @@ -4,6 +4,7 @@ const { Schema } = mongoose; const title = new Schema({ titleName: { type: String, required: true }, + titleCode: { type: String, required: true }, teamCode: { type: String, require: true }, projectAssigned: { projectName: { type: String, required: true }, @@ -13,8 +14,7 @@ const title = new Schema({ teamAssiged: { teamName: { type: String }, _id: { type: String }, - }, - shortName: { type: String, require: true }, + }, }); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index cc7136f54..3a529294a 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -27,6 +27,7 @@ const userProfileSchema = new Schema({ isActive: { type: Boolean, required: true, default: true }, isRehireable: { type: Boolean, default: true }, isSet: { type: Boolean, required: true, default: false }, + finalEmailThreeWeeksSent: { type: Boolean, required: true, default: false }, role: { type: String, required: true, diff --git a/src/routes/timeentryRouter.js b/src/routes/timeentryRouter.js index 0fd7db716..b5fd641ae 100644 --- a/src/routes/timeentryRouter.js +++ b/src/routes/timeentryRouter.js @@ -19,6 +19,14 @@ const routes = function (TimeEntry) { TimeEntryRouter.route('/TimeEntry/reports').post(controller.getTimeEntriesForReports); + TimeEntryRouter.route('/TimeEntry/reports/projects').post( + controller.getTimeEntriesForProjectReports, + ); + + TimeEntryRouter.route('/TimeEntry/reports/people').post( + controller.getTimeEntriesForPeopleReports, + ); + TimeEntryRouter.route('/TimeEntry/lostUsers').post(controller.getLostTimeEntriesForUserList); TimeEntryRouter.route('/TimeEntry/lostProjects').post( @@ -32,9 +40,11 @@ const routes = function (TimeEntry) { ); TimeEntryRouter.route('/TimeEntry/recalculateHoursAllUsers/tangible').post( - controller.recalculateHoursByCategoryAllUsers, + controller.startRecalculation, ); + TimeEntryRouter.route('/TimeEntry/checkStatus/:taskId').get(controller.checkRecalculationStatus); + TimeEntryRouter.route('/TimeEntry/recalculateHoursAllUsers/intangible').post( controller.recalculateIntangibleHrsAllUsers, ); From 2a218e45822869e5092a1ec0bb2fcb403f382188 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 19:25:07 -0800 Subject: [PATCH 33/48] 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 34/48] 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 35/48] 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 36/48] 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 37/48] 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 38/48] 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 0ebdc61f513dacbfbf064c1c32b65325a0bf22a4 Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 21:20:31 -0800 Subject: [PATCH 39/48] Add or update the Azure App Service build and deployment workflow config --- .../workflows/development_hgn-rest-dev.yml | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/.github/workflows/development_hgn-rest-dev.yml b/.github/workflows/development_hgn-rest-dev.yml index 7d822c869..8c1ca1ecc 100644 --- a/.github/workflows/development_hgn-rest-dev.yml +++ b/.github/workflows/development_hgn-rest-dev.yml @@ -1,20 +1,6 @@ # 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: @@ -23,33 +9,54 @@ on: - 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 + build: runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: '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_D3183F132BC14D79A19DC24953125EA4 }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - 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-dev' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_37BDFCF15560478F9069CEF1EA105BF0 }} \ No newline at end of file From 3d26e8f201a53383d654c7fcd38de0b64c50d4ac Mon Sep 17 00:00:00 2001 From: One Community Date: Tue, 10 Dec 2024 21:21:12 -0800 Subject: [PATCH 40/48] Update development_hgn-rest-dev.yml --- .github/workflows/development_hgn-rest-dev.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/development_hgn-rest-dev.yml b/.github/workflows/development_hgn-rest-dev.yml index 8c1ca1ecc..05030cf70 100644 --- a/.github/workflows/development_hgn-rest-dev.yml +++ b/.github/workflows/development_hgn-rest-dev.yml @@ -19,13 +19,12 @@ jobs: - name: Set up Node.js version uses: actions/setup-node@v3 with: - node-version: '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-dev' slot-name: 'Production' package: . - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_37BDFCF15560478F9069CEF1EA105BF0 }} \ No newline at end of file + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_37BDFCF15560478F9069CEF1EA105BF0 }} 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 41/48] 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 }} From e1a583041d33eb54592ea0ef7f6293e916c04620 Mon Sep 17 00:00:00 2001 From: SFA23SCM35V Date: Fri, 13 Dec 2024 11:58:25 -0600 Subject: [PATCH 42/48] commit changes --- src/controllers/actionItemController.js | 251 ++++++++++++------------ 1 file changed, 123 insertions(+), 128 deletions(-) diff --git a/src/controllers/actionItemController.js b/src/controllers/actionItemController.js index 6d5864213..390794b31 100644 --- a/src/controllers/actionItemController.js +++ b/src/controllers/actionItemController.js @@ -1,128 +1,123 @@ -/** - * Unused legacy code. Commented out to avoid confusion. Will delete in the next cycle. - * Commented by: Shengwei Peng - * Date: 2024-03-22 - */ - -// const mongoose = require('mongoose'); -// const notificationhelper = require('../helpers/notificationhelper')(); - -// const actionItemController = function (ActionItem) { -// const getactionItem = function (req, res) { -// const userid = req.params.userId; -// ActionItem.find({ -// assignedTo: userid, -// }, ('-createdDateTime -__v')) -// .populate('createdBy', 'firstName lastName') -// .then((results) => { -// const actionitems = []; - -// results.forEach((element) => { -// const actionitem = {}; - -// actionitem._id = element._id; -// actionitem.description = element.description; -// actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; -// actionitem.assignedTo = element.assignedTo; - -// actionitems.push(actionitem); -// }); - -// res.status(200).send(actionitems); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; -// const postactionItem = function (req, res) { -// const { requestorId, assignedTo } = req.body.requestor; -// const _actionItem = new ActionItem(); - -// _actionItem.description = req.body.description; -// _actionItem.assignedTo = req.body.assignedTo; -// _actionItem.createdBy = req.body.requestor.requestorId; - -// _actionItem.save() -// .then((result) => { -// notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); - -// const actionitem = {}; - -// actionitem.createdBy = 'You'; -// actionitem.description = _actionItem.description; -// actionitem._id = result._id; -// actionitem.assignedTo = _actionItem.assignedTo; - -// res.status(200).send(actionitem); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; - -// const deleteactionItem = async function (req, res) { -// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - -// const _actionItem = await ActionItem.findById(actionItemId) -// .catch((error) => { -// res.status(400).send(error); -// }); - -// if (!_actionItem) { -// res.status(400).send({ -// message: 'No valid records found', -// }); -// return; -// } - -// const { requestorId, assignedTo } = req.body.requestor; - -// notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); - -// _actionItem.remove() -// .then(() => { -// res.status(200).send({ -// message: 'removed', -// }); -// }) -// .catch((error) => { -// res.status(400).send(error); -// }); -// }; - -// const editactionItem = async function (req, res) { -// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - -// const { requestorId, assignedTo } = req.body.requestor; - -// const _actionItem = await ActionItem.findById(actionItemId) -// .catch((error) => { -// res.status(400).send(error); -// }); - -// if (!_actionItem) { -// res.status(400).send({ -// message: 'No valid records found', -// }); -// return; -// } -// notificationhelper.notificationedited(requestorId, assignedTo, _actionItem.description, req.body.description); - -// _actionItem.description = req.body.description; -// _actionItem.assignedTo = req.body.assignedTo; - -// _actionItem.save() -// .then(res.status(200).send('Saved')) -// .catch((error) => res.status(400).send(error)); -// }; - -// return { -// getactionItem, -// postactionItem, -// deleteactionItem, -// editactionItem, - -// }; -// }; - -// module.exports = actionItemController; +const mongoose = require('mongoose'); +const notificationhelper = require('../helpers/notificationhelper')(); + +const actionItemController = function (ActionItem) { + const getactionItem = function (req, res) { + const userid = req.params.userId; + ActionItem.find({ + assignedTo: userid, + }, ('-createdDateTime -__v')) + .populate('createdBy', 'firstName lastName') + .then((results) => { + const actionitems = []; + + results.forEach((element) => { + const actionitem = {}; + + actionitem._id = element._id; + actionitem.description = element.description; + actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; + actionitem.assignedTo = element.assignedTo; + + actionitems.push(actionitem); + }); + + res.status(200).send(actionitems); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const postactionItem = function (req, res) { + const { requestorId, assignedTo } = req.body.requestor; + const _actionItem = new ActionItem(); + + _actionItem.description = req.body.description; + _actionItem.assignedTo = req.body.assignedTo; + _actionItem.createdBy = req.body.requestor.requestorId; + + _actionItem.save() + .then((result) => { + notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); + + const actionitem = {}; + + actionitem.createdBy = 'You'; + actionitem.description = _actionItem.description; + actionitem._id = result._id; + actionitem.assignedTo = _actionItem.assignedTo; + + res.status(200).send(actionitem); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const deleteactionItem = async function (req, res) { + const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + + const _actionItem = await ActionItem.findById(actionItemId) + .catch((error) => { + res.status(400).send(error); + }); + + if (!_actionItem) { + res.status(400).send({ + message: 'No valid records found', + }); + return; + } + + const { requestorId, assignedTo } = req.body.requestor; + + notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); + + _actionItem.remove() + .then(() => { + res.status(200).send({ + message: 'removed', + }); + }) + .catch((error) => { + res.status(400).send(error); + }); + }; + + const editactionItem = async function (req, res) { + const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + + const { requestorId, assignedTo } = req.body.requestor; + + const _actionItem = await ActionItem.findById(actionItemId) + .catch((error) => { + res.status(400).send(error); + }); + + if (!_actionItem) { + res.status(400).send({ + message: 'No valid records found', + }); + return; + } + + notificationhelper.notificationedited(requestorId, assignedTo, _actionItem.description, req.body.description); + + _actionItem.description = req.body.description; + _actionItem.assignedTo = req.body.assignedTo; + + _actionItem.save() + .then(res.status(200).send('Saved')) + .catch((error) => res.status(400).send(error)); + }; + + return { + getactionItem, + postactionItem, + deleteactionItem, + editactionItem, + }; +}; + +module.exports = actionItemController; From f3d2922da4f3191c75b419a489462558a11d1bb4 Mon Sep 17 00:00:00 2001 From: Strallia Chao <134454347+strallia@users.noreply.github.com> Date: Sat, 14 Dec 2024 10:17:41 -0800 Subject: [PATCH 43/48] hotfix: fix some tasks not showing on dashboard's tasks list --- src/controllers/projectController.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 72522ba80..3e48b3f42 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -291,12 +291,21 @@ const projectController = function (Project) { res.status(400).send('Invalid request'); return; } - const getId = await hasPermission(req.body.requestor, 'getProjectMembers'); + const getProjMembers = await hasPermission(req.body.requestor, 'getProjectMembers'); + + // If a user has permission to post, edit, or suggest tasks, they also have the ability to assign resources to those tasks. + // Therefore, the _id field must be included when retrieving the user profile for project members (resources). + const postTask = await hasPermission(req.body.requestor, 'postTask'); + const updateTask = await hasPermission(req.body.requestor, 'updateTask'); + const suggestTask = await hasPermission(req.body.requestor, 'suggestTask'); + + const canGetId = (getProjMembers || postTask || updateTask || suggestTask); + userProfile .find( { projects: projectId }, - { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: getId }, + { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: canGetId }, ) .sort({ firstName: 1, lastName: 1 }) .then((results) => { From 4146d0899f1798e851d1532ba5f9119275db40d2 Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:15:31 -0500 Subject: [PATCH 44/48] fix: fully revert changes in #1128. --- package-lock.json | 608 ++++--- package.json | 1 + .../addNonHgnEmailSubscription.md | 23 + .../confirmNonHgnEmailSubscription.md | 18 + .../removeNonHgnEmailSubscription.md | 10 + requirements/emailController/sendEmail.md | 10 + .../emailController/sendEmailToAll.md | 26 + .../updateEmailSubscription.md | 20 + .../informationController/addInformation.md | 16 + .../deleteInformation.md | 13 + .../informationController/getInformation.md | 13 + .../updateInformation.md | 13 + .../createPopPopupEditor.md | 14 + .../getAllPopupEditors.md | 10 + .../getPopupEditorById.md | 10 + .../updatePopupEditor.md | 11 + .../deleteReason.md | 16 + .../getAllReasons.md | 14 + .../getSingleReason.md | 15 + .../reasonSchedulingController/patchReason.md | 16 + .../reasonSchedulingController/postReason.md | 18 + requirements/taskController/deleteTask.md | 15 + .../taskController/deleteTaskByWBS.md | 15 + requirements/taskController/fixTasks.md | 11 + requirements/taskController/getTaskById.md | 14 + requirements/taskController/getTasks.md | 12 + .../taskController/getTasksByUserId.md | 12 + .../taskController/getTasksForTeamsByUser.md | 12 + requirements/taskController/getWBSId.md | 12 + requirements/taskController/importTask.md | 14 + requirements/taskController/moveTask.md | 13 + requirements/taskController/postTask.md | 14 + requirements/taskController/sendReviewReq.md | 12 + requirements/taskController/swap.md | 16 + .../taskController/updateAllParents.md | 12 + requirements/taskController/updateNum.md | 16 + requirements/taskController/updateTask.md | 16 + .../taskController/updateTaskStatus.md | 12 + .../timeZoneAPIController/getTImeZone.md | 20 + .../getTimeZoneProfileInitialSetup.md | 20 + src/controllers/emailController.js | 148 +- src/controllers/emailController.spec.js | 146 ++ src/controllers/informationController.js | 6 +- src/controllers/informationController.spec.js | 392 +++++ src/controllers/jobsController.js | 131 ++ src/controllers/logincontroller.js | 2 +- src/controllers/logincontroller.spec.js | 2 +- src/controllers/ownerMessageController.js | 13 +- src/controllers/popupEditorController.spec.js | 163 ++ .../profileInitialSetupController.js | 67 +- .../reasonSchedulingController.spec.js | 626 +++++++ src/controllers/taskController.js | 5 +- src/controllers/taskController.spec.js | 1555 +++++++++++++++++ src/controllers/teamController.js | 163 +- src/controllers/timeZoneAPIController.js | 13 +- src/controllers/timeZoneAPIController.spec.js | 316 ++++ src/cronjobs/userProfileJobs.js | 11 + src/models/jobs.js | 17 + src/routes/informationRouter.test.js | 145 ++ src/routes/jobsRouter.js | 14 + src/routes/reasonRouter.test.js | 338 ++++ src/routes/teamRouter.js | 4 + src/routes/timeZoneAPIRoutes.test.js | 208 +++ src/startup/db.js | 2 +- src/startup/middleware.js | 34 +- src/startup/routes.js | 3 +- src/test/createTestPermissions.js | 18 +- src/utilities/createInitialPermissions.js | 16 +- src/utilities/emailSender.js | 149 +- 69 files changed, 5402 insertions(+), 468 deletions(-) create mode 100644 requirements/emailController/addNonHgnEmailSubscription.md create mode 100644 requirements/emailController/confirmNonHgnEmailSubscription.md create mode 100644 requirements/emailController/removeNonHgnEmailSubscription.md create mode 100644 requirements/emailController/sendEmail.md create mode 100644 requirements/emailController/sendEmailToAll.md create mode 100644 requirements/emailController/updateEmailSubscription.md create mode 100644 requirements/informationController/addInformation.md create mode 100644 requirements/informationController/deleteInformation.md create mode 100644 requirements/informationController/getInformation.md create mode 100644 requirements/informationController/updateInformation.md create mode 100644 requirements/popUpEditorController/createPopPopupEditor.md create mode 100644 requirements/popUpEditorController/getAllPopupEditors.md create mode 100644 requirements/popUpEditorController/getPopupEditorById.md create mode 100644 requirements/popUpEditorController/updatePopupEditor.md create mode 100644 requirements/reasonSchedulingController/deleteReason.md create mode 100644 requirements/reasonSchedulingController/getAllReasons.md create mode 100644 requirements/reasonSchedulingController/getSingleReason.md create mode 100644 requirements/reasonSchedulingController/patchReason.md create mode 100644 requirements/reasonSchedulingController/postReason.md create mode 100644 requirements/taskController/deleteTask.md create mode 100644 requirements/taskController/deleteTaskByWBS.md create mode 100644 requirements/taskController/fixTasks.md create mode 100644 requirements/taskController/getTaskById.md create mode 100644 requirements/taskController/getTasks.md create mode 100644 requirements/taskController/getTasksByUserId.md create mode 100644 requirements/taskController/getTasksForTeamsByUser.md create mode 100644 requirements/taskController/getWBSId.md create mode 100644 requirements/taskController/importTask.md create mode 100644 requirements/taskController/moveTask.md create mode 100644 requirements/taskController/postTask.md create mode 100644 requirements/taskController/sendReviewReq.md create mode 100644 requirements/taskController/swap.md create mode 100644 requirements/taskController/updateAllParents.md create mode 100644 requirements/taskController/updateNum.md create mode 100644 requirements/taskController/updateTask.md create mode 100644 requirements/taskController/updateTaskStatus.md create mode 100644 requirements/timeZoneAPIController/getTImeZone.md create mode 100644 requirements/timeZoneAPIController/getTimeZoneProfileInitialSetup.md create mode 100644 src/controllers/emailController.spec.js create mode 100644 src/controllers/informationController.spec.js create mode 100644 src/controllers/jobsController.js create mode 100644 src/controllers/popupEditorController.spec.js create mode 100644 src/controllers/reasonSchedulingController.spec.js create mode 100644 src/controllers/taskController.spec.js create mode 100644 src/controllers/timeZoneAPIController.spec.js create mode 100644 src/models/jobs.js create mode 100644 src/routes/informationRouter.test.js create mode 100644 src/routes/jobsRouter.js create mode 100644 src/routes/reasonRouter.test.js create mode 100644 src/routes/timeZoneAPIRoutes.test.js diff --git a/package-lock.json b/package-lock.json index 3c4e3e99e..9e07004c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -619,9 +619,9 @@ }, "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==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true } } @@ -691,18 +691,18 @@ } }, "@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==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "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==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true } } @@ -1477,9 +1477,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -1593,9 +1593,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -1710,9 +1710,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -1825,9 +1825,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -1911,9 +1911,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -2023,9 +2023,9 @@ "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "@jridgewell/trace-mapping": { @@ -2039,9 +2039,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -2131,9 +2131,9 @@ "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "@jridgewell/trace-mapping": { @@ -2175,9 +2175,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -2298,9 +2298,9 @@ "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "@jridgewell/trace-mapping": { @@ -2314,9 +2314,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -2857,7 +2857,7 @@ "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/methods": { @@ -3081,7 +3081,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { "version": "3.1.6", @@ -4739,7 +4739,7 @@ "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, "bignumber.js": { "version": "9.0.2", @@ -4797,6 +4797,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4869,7 +4874,7 @@ "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-from": { "version": "1.1.2", @@ -4923,6 +4928,33 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4945,9 +4977,9 @@ "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": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true }, "clean-stack": { @@ -5036,7 +5068,7 @@ "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, "clone-deep": { "version": "4.0.1", @@ -5076,7 +5108,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { "version": "2.0.19", @@ -5100,7 +5132,7 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "component-emitter": { "version": "1.3.1", @@ -5110,7 +5142,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "confusing-browser-globals": { "version": "1.0.11", @@ -5154,7 +5186,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { "version": "2.1.4", @@ -5226,9 +5258,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -5321,6 +5353,23 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5616,7 +5665,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { "version": "1.4.81", @@ -5638,7 +5687,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.4", @@ -5783,12 +5832,12 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "8.47.0", @@ -6718,7 +6767,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "event-target-shim": { "version": "5.0.1", @@ -6881,7 +6930,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "safe-buffer": { "version": "5.2.1", @@ -6926,7 +6975,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "fast-safe-stringify": { @@ -7000,7 +7049,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -7137,7 +7186,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-constants": { "version": "1.0.0", @@ -7153,7 +7202,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.3", @@ -7460,7 +7509,7 @@ "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==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -7609,9 +7658,9 @@ } }, "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "requires": { "pkg-dir": "^4.2.0", @@ -7666,7 +7715,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "indent-string": { @@ -7678,7 +7727,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7872,7 +7921,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -7986,13 +8035,13 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "istanbul-lib-coverage": { "version": "3.2.2", @@ -8001,9 +8050,9 @@ "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==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "requires": { "@babel/core": "^7.23.9", @@ -8034,27 +8083,27 @@ } }, "@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "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==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "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", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -8071,26 +8120,26 @@ } }, "@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, "requires": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.25.6", "@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==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "requires": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -8103,34 +8152,6 @@ } } }, - "@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", @@ -8142,16 +8163,15 @@ } }, "@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==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "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-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" } }, "@babel/helper-simple-access": { @@ -8164,19 +8184,10 @@ "@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==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true }, "@babel/helper-validator-identifier": { @@ -8186,19 +8197,19 @@ "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==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "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==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, "requires": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" } }, "@babel/highlight": { @@ -8214,47 +8225,47 @@ } }, "@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 + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.25.6" + } }, "@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "requires": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" } }, "@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "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", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", "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==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" } @@ -8293,23 +8304,23 @@ }, "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true } } }, "browserslist": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", - "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "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" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" } }, "convert-source-map": { @@ -8319,9 +8330,9 @@ "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==", + "version": "1.5.22", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.22.tgz", + "integrity": "sha512-tKYm5YHPU1djz0O+CGJ+oJIvimtsCcwR2Z9w7Skh08lUdyzXY5djods3q+z2JkWdb7tCcmM//eVavSRAiaPRNg==", "dev": true }, "lru-cache": { @@ -8334,15 +8345,15 @@ } }, "node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true }, "yallist": { @@ -8380,9 +8391,9 @@ } }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true }, "supports-color": { @@ -8452,9 +8463,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -8633,9 +8644,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -8777,9 +8788,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -8973,9 +8984,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9092,9 +9103,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9192,9 +9203,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9436,9 +9447,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9552,9 +9563,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9751,9 +9762,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9880,9 +9891,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -9994,9 +10005,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -10092,9 +10103,9 @@ "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true }, "supports-color": { @@ -10137,9 +10148,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -10225,9 +10236,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -10352,9 +10363,9 @@ } }, "@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -10482,7 +10493,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, "json5": { @@ -10837,7 +10848,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.merge": { "version": "4.6.2", @@ -11036,7 +11047,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memory-pager": { "version": "1.5.0", @@ -11047,7 +11058,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge-stream": { "version": "2.0.0", @@ -11058,7 +11069,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { "version": "4.0.5", @@ -11385,7 +11396,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, "negotiator": { @@ -11549,10 +11560,18 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "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==" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { "version": "1.12.0", @@ -12271,7 +12290,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } @@ -12361,13 +12380,30 @@ "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==" + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, "parse-srcset": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12376,12 +12412,12 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -12397,7 +12433,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "pend": { "version": "1.2.0", @@ -13150,7 +13186,7 @@ "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", "optional": true, "requires": { "memory-pager": "^1.0.2" @@ -13900,7 +13936,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, "strip-final-newline": { @@ -14091,13 +14127,13 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "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": { @@ -14115,7 +14151,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-regex-range": { "version": "5.0.1", @@ -14142,7 +14178,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tsconfig-paths": { "version": "3.14.2", @@ -14308,12 +14344,12 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "requires": { "escalade": "^3.1.2", @@ -14321,15 +14357,15 @@ }, "dependencies": { "escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, "picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true } } @@ -14346,17 +14382,17 @@ "url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.4.0", @@ -14381,9 +14417,9 @@ "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==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "@jridgewell/trace-mapping": { @@ -14420,7 +14456,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "walker": { "version": "1.0.8", @@ -14434,12 +14470,12 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -14557,7 +14593,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "4.0.2", diff --git a/package.json b/package.json index 8b88744dc..91e73f39a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "babel-plugin-module-resolver": "^5.0.0", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", + "cheerio": "^1.0.0-rc.12", "cors": "^2.8.4", "cron": "^1.8.2", "dotenv": "^5.0.1", diff --git a/requirements/emailController/addNonHgnEmailSubscription.md b/requirements/emailController/addNonHgnEmailSubscription.md new file mode 100644 index 000000000..f5748f142 --- /dev/null +++ b/requirements/emailController/addNonHgnEmailSubscription.md @@ -0,0 +1,23 @@ +# Add Non-HGN Email Subscription Function + +## Negative Cases + +1. ❌ **Returns error 400 if `email` field is missing from the request** + - Ensures that the function checks for the presence of the `email` field in the request body and responds with a `400` status code if it's missing. + +2. ❌ **Returns error 400 if the provided `email` already exists in the subscription list** + - This case checks that the function responds with a `400` status code and a message indicating that the email is already subscribed. + +3. ❌ **Returns error 500 if there is an internal error while checking the subscription list** + - Covers scenarios where there's an issue querying the `EmailSubscriptionList` collection for the provided email (e.g., database connection issues). + +4. ❌ **Returns error 500 if there is an error sending the confirmation email** + - This case handles any issues that occur while calling the `emailSender` function, such as network errors or service unavailability. + +## Positive Cases + +1. ❌ **Returns status 200 when a new email is successfully subscribed** + - Ensures that the function successfully creates a JWT token, constructs the email, and sends the subscription confirmation email to the user. + +2. ❌ **Successfully sends a confirmation email containing the correct link** + - Verifies that the generated JWT token is correctly included in the confirmation link sent to the user in the email body. diff --git a/requirements/emailController/confirmNonHgnEmailSubscription.md b/requirements/emailController/confirmNonHgnEmailSubscription.md new file mode 100644 index 000000000..d5e1367af --- /dev/null +++ b/requirements/emailController/confirmNonHgnEmailSubscription.md @@ -0,0 +1,18 @@ +# Confirm Non-HGN Email Subscription Function Tests + +## Negative Cases +1. ✅ **Returns error 400 if `token` field is missing from the request** + - (Test: `should return 400 if token is not provided`) + +2. ✅ **Returns error 401 if the provided `token` is invalid or expired** + - (Test: `should return 401 if token is invalid`) + +3. ✅ **Returns error 400 if the decoded `token` does not contain a valid `email` field** + - (Test: `should return 400 if email is missing from payload`) + +4. ❌ **Returns error 500 if there is an internal error while saving the new email subscription** + +## Positive Cases +1. ❌ **Returns status 200 when a new email is successfully subscribed** + +2. ❌ **Returns status 200 if the email is already subscribed (duplicate email)** diff --git a/requirements/emailController/removeNonHgnEmailSubscription.md b/requirements/emailController/removeNonHgnEmailSubscription.md new file mode 100644 index 000000000..af793e2a9 --- /dev/null +++ b/requirements/emailController/removeNonHgnEmailSubscription.md @@ -0,0 +1,10 @@ +# Remove Non-HGN Email Subscription Function Tests + +## Negative Cases +1. ✅ **Returns error 400 if `email` field is missing from the request** + - (Test: `should return 400 if email is missing`) + +2. ❌ **Returns error 500 if there is an internal error while deleting the email subscription** + +## Positive Cases +1. ❌ **Returns status 200 when an email is successfully unsubscribed** diff --git a/requirements/emailController/sendEmail.md b/requirements/emailController/sendEmail.md new file mode 100644 index 000000000..7ca9a482c --- /dev/null +++ b/requirements/emailController/sendEmail.md @@ -0,0 +1,10 @@ +# Send Email Function + +## Negative Cases + +1. ❌ **Returns error 400 if `to`, `subject`, or `html` fields are missing from the request** +2. ❌ **Returns error 500 if there is an internal error while sending the email** + +## Positive Cases + +1. ✅ **Returns status 200 when email is successfully sent with `to`, `subject`, and `html` fields provided** diff --git a/requirements/emailController/sendEmailToAll.md b/requirements/emailController/sendEmailToAll.md new file mode 100644 index 000000000..32a09fed6 --- /dev/null +++ b/requirements/emailController/sendEmailToAll.md @@ -0,0 +1,26 @@ +# Send Email to All Function + +## Negative Cases + +1. ❌ **Returns error 400 if `subject` or `html` fields are missing from the request** + - The request should be rejected if either the `subject` or `html` content is not provided in the request body. + +2. ❌ **Returns error 500 if there is an internal error while fetching users** + - This case covers scenarios where there's an error fetching users from the `userProfile` collection (e.g., database connection issues). + +3. ❌ **Returns error 500 if there is an internal error while fetching the subscription list** + - This case covers scenarios where there's an error fetching emails from the `EmailSubcriptionList` collection. + +4. ❌ **Returns error 500 if there is an error sending emails** + - This case handles any issues that occur while calling the `emailSender` function, such as network errors or service unavailability. + +## Positive Cases + +1. ❌ **Returns status 200 when emails are successfully sent to all active users** + - Ensures that the function sends emails correctly to all users meeting the criteria (`isActive` and `EmailSubcriptionList`). + +2. ❌ **Returns status 200 when emails are successfully sent to all users in the subscription list** + - Verifies that the function sends emails to all users in the `EmailSubcriptionList`, including the unsubscribe link in the email body. + +3. ❌ **Combines user and subscription list emails successfully** + - Ensures that the function correctly sends emails to both active users and the subscription list without issues. diff --git a/requirements/emailController/updateEmailSubscription.md b/requirements/emailController/updateEmailSubscription.md new file mode 100644 index 000000000..bcafa5a28 --- /dev/null +++ b/requirements/emailController/updateEmailSubscription.md @@ -0,0 +1,20 @@ +# Update Email Subscriptions Function + +## Negative Cases + +1. ❌ **Returns error 400 if `emailSubscriptions` field is missing from the request** + - This ensures that the function checks for the presence of the `emailSubscriptions` field in the request body and responds with a `400` status code if it's missing. + +2. ❌ **Returns error 400 if `email` field is missing from the requestor object** + - Ensures that the function requires an `email` field within the `requestor` object in the request body and returns `400` if it's absent. + +3. ❌ **Returns error 404 if the user with the provided `email` is not found** + - This checks that the function correctly handles cases where no user exists with the given `email` and responds with a `404` status code. + +4. ✅ **Returns error 500 if there is an internal error while updating the user profile** + - Covers scenarios where there's a database error while updating the user's email subscriptions. + +## Positive Cases + +1. ❌ **Returns status 200 and the updated user when email subscriptions are successfully updated** + - Ensures that the function updates the `emailSubscriptions` field for the user and returns the updated user document along with a `200` status code. diff --git a/requirements/informationController/addInformation.md b/requirements/informationController/addInformation.md new file mode 100644 index 000000000..d62066735 --- /dev/null +++ b/requirements/informationController/addInformation.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Add Information + +> ## Positive case +1. ✅ Returns 201 if adding new information successfullyn and no cache. +2. ✅ Returns if adding new information successfully and hascache. + +> ## Negative case +1. ✅ Returns error 500 if if there are no information in the database and any error occurs when finding the infoName. +2. ✅ Returns error 400 if if there are duplicate infoName in the database. +3. ✅ Returns error 400 if if there are issues when saving new informations. +4. ✅ Returns error 400 if if there are errors when saving the new information. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/deleteInformation.md b/requirements/informationController/deleteInformation.md new file mode 100644 index 000000000..fb5c3c867 --- /dev/null +++ b/requirements/informationController/deleteInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Delete Information + +> ## Positive case +1. ✅ Returns 200 if deleting informations successfull and no cache. +2. ✅ Returns if deleting informations successfully and has cache. + +> ## Negative case +1. ✅ Returns error 400 if if there is any error when finding the information by information id. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/getInformation.md b/requirements/informationController/getInformation.md new file mode 100644 index 000000000..bd8976a5a --- /dev/null +++ b/requirements/informationController/getInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Information + +> ## Positive case +1. ✅ Returns 200 if the informations key exists in NodeCache. +2. ✅ Returns 200 if there are information in the database. + +> ## Negative case +1. ✅ Returns error 404 if if there are no information in the database and any error occurs when finding the information. + +> ## Edge case \ No newline at end of file diff --git a/requirements/informationController/updateInformation.md b/requirements/informationController/updateInformation.md new file mode 100644 index 000000000..709bcba54 --- /dev/null +++ b/requirements/informationController/updateInformation.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Update Information + +> ## Positive case +1. ✅ Returns 200 if updating informations successfully when no cache. +2. ✅ Returns if updating informations successfully when hascache. + +> ## Negative case +1. ✅ Returns error 400 if if there is any error when finding the information by information id. + +> ## Edge case \ No newline at end of file diff --git a/requirements/popUpEditorController/createPopPopupEditor.md b/requirements/popUpEditorController/createPopPopupEditor.md new file mode 100644 index 000000000..0afcaa740 --- /dev/null +++ b/requirements/popUpEditorController/createPopPopupEditor.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createPopPopupEditor Function + +> ### Positive case + +> 1. ✅ Should return 201 and the new pop-up editor on success + +> ### Negative case + +> 1. ✅ Should return 403 if user does not have permission to create a pop-up editor +> 2. ✅ Should return 400 if the request body is missing required fields +> 3. ✅ Should return 500 if there is an error saving the new pop-up editor to the database diff --git a/requirements/popUpEditorController/getAllPopupEditors.md b/requirements/popUpEditorController/getAllPopupEditors.md new file mode 100644 index 000000000..f42f93c1a --- /dev/null +++ b/requirements/popUpEditorController/getAllPopupEditors.md @@ -0,0 +1,10 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getAllPopupEditors Function + +> ## Positive case +> 1. ✅ Should return 200 and all pop-up editors on success + +> ## Negative case +> 1. ✅ Should return 404 if there is an error retrieving the pop-up editors from the database diff --git a/requirements/popUpEditorController/getPopupEditorById.md b/requirements/popUpEditorController/getPopupEditorById.md new file mode 100644 index 000000000..013096ed9 --- /dev/null +++ b/requirements/popUpEditorController/getPopupEditorById.md @@ -0,0 +1,10 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getPopupEditorById Function + +> ## Positive case +> 1. ✅ Should return 200 and the pop-up editor on success + +> ## Negative case +> 1. ✅ Should return 404 if the pop-up editor is not found diff --git a/requirements/popUpEditorController/updatePopupEditor.md b/requirements/popUpEditorController/updatePopupEditor.md new file mode 100644 index 000000000..c4e5f5904 --- /dev/null +++ b/requirements/popUpEditorController/updatePopupEditor.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updatePopupEditor Function + +> ## Positive case +> 1. ✅ Should return 200 and the updated pop-up editor on success + + +> ## Negative case +> 1. ✅ Should return 404 if the pop-up editor is not found diff --git a/requirements/reasonSchedulingController/deleteReason.md b/requirements/reasonSchedulingController/deleteReason.md new file mode 100644 index 000000000..8df5ccb16 --- /dev/null +++ b/requirements/reasonSchedulingController/deleteReason.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deleteReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/reason/:userId/** route. +2. ✅ Return 200 if delete reason successfully. + +> ## Negative case +1. ✅ Returns 403 when no permission to delete. +2. ✅ Returns 404 when error in finding user Id. +3. ✅ Returns 404 when error in finding reason. +4. ✅ Returns 500 when error in deleting. + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/getAllReasons.md b/requirements/reasonSchedulingController/getAllReasons.md new file mode 100644 index 000000000..58499a41b --- /dev/null +++ b/requirements/reasonSchedulingController/getAllReasons.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getAllReasons + +> ## Positive case +1. ✅ Receives a GET request in the **/api/reason/:userId** route. +2. ✅ Return 200 if get schedule reason successfully. + +> ## Negative case +1. ✅ Returns 404 when error in finding user by Id. +2. ✅ Returns 400 when any error in fetching the user + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/getSingleReason.md b/requirements/reasonSchedulingController/getSingleReason.md new file mode 100644 index 000000000..bc81fd9d9 --- /dev/null +++ b/requirements/reasonSchedulingController/getSingleReason.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getSingleReason + +> ## Positive case +1. ✅ Receives a GET request in the **/api/reason/single/:userId** route. +2. ✅ Return 200 if not found schedule reason and return empty object successfully. +3. ✅ Return 200 if found schedule reason and return reason successfully. + +> ## Negative case +1. ✅ Returns 404 when any error in find user by Id +2. ✅ Returns 400 when any error in fetching the user + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/patchReason.md b/requirements/reasonSchedulingController/patchReason.md new file mode 100644 index 000000000..6e84a8ba7 --- /dev/null +++ b/requirements/reasonSchedulingController/patchReason.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# patchReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/breason/** route. +2. ✅ Return 200 if updated schedule reason and send blue sqaure email successfully. + +> ## Negative case +1. ✅ Returns 400 for not providing reason. +2. ✅ Returns 404 when error in finding user Id. +3. ✅ Returns 404 when not finding provided reason. +4. ✅ Returns 400 when any error in saving. + +> ## Edge case \ No newline at end of file diff --git a/requirements/reasonSchedulingController/postReason.md b/requirements/reasonSchedulingController/postReason.md new file mode 100644 index 000000000..ac8ea8f2d --- /dev/null +++ b/requirements/reasonSchedulingController/postReason.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# postReason + +> ## Positive case +1. ✅ Receives a POST request in the **/api/reason/** route. +2. ✅ Return 200 if s dchedule reason and send blue sqaure email successfully. + +> ## Negative case +1. ✅ Returns 400 for warning to choose Sunday. +2. ✅ Returns 400 for warning to choose a funture date. +3. ✅ Returns 400 for not providing reason. +4. ✅ Returns 404 when error in finding user Id. +5. ✅ Returns 403 when duplicate reason to the date. +6. ✅ Returns 400 when any error in saving. + +> ## Edge case \ No newline at end of file diff --git a/requirements/taskController/deleteTask.md b/requirements/taskController/deleteTask.md new file mode 100644 index 000000000..4f986e2a9 --- /dev/null +++ b/requirements/taskController/deleteTask.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deleteTask Function + +> ## Positive case +1. ✅ Returns status 200 on successful deletion. + +> ## Negative case +1. ✅ Returns status 400 if either no record is found in Task collection or some error occurs while saving the tasks. + +2. ✅ Returns status 403 if the request.body.requestor does not have `deleteTask` permission. + + +> ## Edge case diff --git a/requirements/taskController/deleteTaskByWBS.md b/requirements/taskController/deleteTaskByWBS.md new file mode 100644 index 000000000..20ef90624 --- /dev/null +++ b/requirements/taskController/deleteTaskByWBS.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deleteTaskByWBS Function + +> ## Positive case +1. ✅ Returns status 200 on successful deletion. + +> ## Negative case +1. ✅ Returns status 400 if either no record is found in Task collection or some error occurs while saving the tasks. + +2. ✅ Returns status 403 if the request.body.requestor does not have `deleteTask` permission. + + +> ## Edge case diff --git a/requirements/taskController/fixTasks.md b/requirements/taskController/fixTasks.md new file mode 100644 index 000000000..f6a80846a --- /dev/null +++ b/requirements/taskController/fixTasks.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateAllParents Function + +> ## Positive case +1. ✅ Returns status 200 on without performing an operation. + +> ## Negative case + +> ## Edge case diff --git a/requirements/taskController/getTaskById.md b/requirements/taskController/getTaskById.md new file mode 100644 index 000000000..88e7e1d03 --- /dev/null +++ b/requirements/taskController/getTaskById.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getTaskById Function + +> ## Positive case +1. ✅ Returns status 200 on successfully getting a taskById. + +> ## Negative case +1. ✅ Returns status 400 if either req.params.id is missing or is `undefined` or if no task is found in Task collection. + +2. ✅ Returns status 500 if some error occurs. + +> ## Edge case diff --git a/requirements/taskController/getTasks.md b/requirements/taskController/getTasks.md new file mode 100644 index 000000000..8cd6f08e7 --- /dev/null +++ b/requirements/taskController/getTasks.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getTasks Function + +> ## Positive case +1. ✅ Returns status 200 on successfully querying the Task collection. + +> ## Negative case +1. ✅ Returns status 404 if any error occurs while querying the Task collection. + +> ## Edge case diff --git a/requirements/taskController/getTasksByUserId.md b/requirements/taskController/getTasksByUserId.md new file mode 100644 index 000000000..7f482ef07 --- /dev/null +++ b/requirements/taskController/getTasksByUserId.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getTasksByUserId Function + +> ## Positive case +1. ✅ Returns status 200 on successfully getting the tasks. + +> ## Negative case +1. ✅ Returns status 400 if some error occurs. + +> ## Edge case diff --git a/requirements/taskController/getTasksForTeamsByUser.md b/requirements/taskController/getTasksForTeamsByUser.md new file mode 100644 index 000000000..4eae8defa --- /dev/null +++ b/requirements/taskController/getTasksForTeamsByUser.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getTasksForTeamsByUser Function + +> ## Positive case +1. ✅ Returns status 200 on successfully fetching teams data. + +> ## Negative case +1. ✅ Returns status 400 if some error occurs. + +> ## Edge case diff --git a/requirements/taskController/getWBSId.md b/requirements/taskController/getWBSId.md new file mode 100644 index 000000000..8efb2290a --- /dev/null +++ b/requirements/taskController/getWBSId.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getWBSId Function + +> ## Positive case +1. ✅ Returns status 200 on successfully querying the WBS collection. + +> ## Negative case +1. ✅ Returns status 404 if any error occurs while querying the WBS collection. + +> ## Edge case diff --git a/requirements/taskController/importTask.md b/requirements/taskController/importTask.md new file mode 100644 index 000000000..8626209aa --- /dev/null +++ b/requirements/taskController/importTask.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# importTask Function + +> ## Positive case +1. ✅ Returns status 201 on successfully creating and saving the new Task. + +> ## Negative case +1. ✅ Returns status 400 if any error occurs while saving the Task. + +2. ✅ Returns status 403 if request.body.requestor is missing permission for importTask. + +> ## Edge case diff --git a/requirements/taskController/moveTask.md b/requirements/taskController/moveTask.md new file mode 100644 index 000000000..68794e051 --- /dev/null +++ b/requirements/taskController/moveTask.md @@ -0,0 +1,13 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateNum Function + +> ## Positive case +1. ✅ Returns status 200 on successful updation. + +> ## Negative case +1. ✅ Returns status 400 if either request.body.fromNum request.body.toNum is missing or some error occurs while saving the tasks. + + +> ## Edge case diff --git a/requirements/taskController/postTask.md b/requirements/taskController/postTask.md new file mode 100644 index 000000000..971b9a04a --- /dev/null +++ b/requirements/taskController/postTask.md @@ -0,0 +1,14 @@ +Check mark: ✅ +Cross Mark: ❌ + +# postTask Function + +> ## Positive case +1. ✅ Returns status 201 on successfully posting the new task. + +> ## Negative case +1. ✅ Returns status 400 if either request.body.taskName is missing or request.body.isActive is missing or some error occurs while saving task or Wbs or project. + +2. ✅ Returns status 403 if request.body.requestor is missing permission `postTask`. + +> ## Edge case diff --git a/requirements/taskController/sendReviewReq.md b/requirements/taskController/sendReviewReq.md new file mode 100644 index 000000000..53c03ff77 --- /dev/null +++ b/requirements/taskController/sendReviewReq.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# sendReviewReq Function + +> ## Positive case +1. ✅ Returns status 200 on successful operation. + +> ## Negative case +1. ✅ Returns status 500 if some error occurs. + +> ## Edge case diff --git a/requirements/taskController/swap.md b/requirements/taskController/swap.md new file mode 100644 index 000000000..93540de92 --- /dev/null +++ b/requirements/taskController/swap.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# swap Function + +> ## Positive case +1. ✅ Returns status 201 on successfully updating. + +> ## Negative case +1. ✅ Returns status 400 if either request.body.taskId1 is missing or request.body.taskId2 is missing or there is error while executing findById in Task or some error occurs while saving task. + +2. ✅ Returns status 403 if request.body.requestor is missing `swapTask` permission. + +3. ✅ Returns status 404 if some error occurs while executing find operation on Task collection. + +> ## Edge case diff --git a/requirements/taskController/updateAllParents.md b/requirements/taskController/updateAllParents.md new file mode 100644 index 000000000..eccce4d41 --- /dev/null +++ b/requirements/taskController/updateAllParents.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateAllParents Function + +> ## Positive case +1. ✅ Returns status 200 on successful updation. + +> ## Negative case +1. ✅ Returns status 400 if some error occurs. - Not possible to check as per current structure + +> ## Edge case diff --git a/requirements/taskController/updateNum.md b/requirements/taskController/updateNum.md new file mode 100644 index 000000000..4b489014c --- /dev/null +++ b/requirements/taskController/updateNum.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateNum Function + +> ## Positive case +1. ✅ Returns status 200 on successfully updating. + +> ## Negative case +1. ✅ Returns status 400 if either request.body.nums is missing or some error occurs while saving child task. + +2. ✅ Returns status 403 if request.body.requestor is missing `updateNum` permission. + +3. ✅ Returns status 404 if some error occurs while processing the child tasks. + +> ## Edge case diff --git a/requirements/taskController/updateTask.md b/requirements/taskController/updateTask.md new file mode 100644 index 000000000..09e631603 --- /dev/null +++ b/requirements/taskController/updateTask.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateTask Function + +> ## Positive case +1. ✅ Returns status 201 on successfully updating. + +> ## Negative case +1. ✅ Returns status 400 if either request.body.nums is missing or some error occurs while saving child task. + +2. ✅ Returns status 403 if request.body.requestor is missing `updateTask` permission. + +3. ✅ Returns status 404 if some error occurs while executing findOneAndUpdate operation on Task collection. + +> ## Edge case diff --git a/requirements/taskController/updateTaskStatus.md b/requirements/taskController/updateTaskStatus.md new file mode 100644 index 000000000..59bdbdd7c --- /dev/null +++ b/requirements/taskController/updateTaskStatus.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateTaskStatus Function + +> ## Positive case +1. ✅ Returns status 201 on successful update operation. + +> ## Negative case +1. ✅ Returns status 404 if some error occurs. + +> ## Edge case diff --git a/requirements/timeZoneAPIController/getTImeZone.md b/requirements/timeZoneAPIController/getTImeZone.md new file mode 100644 index 000000000..c7ae7801e --- /dev/null +++ b/requirements/timeZoneAPIController/getTImeZone.md @@ -0,0 +1,20 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Time Zone + +> ## Positive case + +1. ✅ Returns status code 200 and response data as follows: + i. current location + ii. timezone + +> ## Negative case + +1. ✅ Returns status code 403, if the user is not authorised. +2. ✅ Returns status code 401, if the API key is missing. +3. ✅ Returns status code 400, if the location is missing. +4. ✅ Returns status code 404, if geocodeAPIEndpoint returns no results. +5. ✅ Returns status code 500, if any other error occurs. + +> ## Edge case diff --git a/requirements/timeZoneAPIController/getTimeZoneProfileInitialSetup.md b/requirements/timeZoneAPIController/getTimeZoneProfileInitialSetup.md new file mode 100644 index 000000000..1a0f91265 --- /dev/null +++ b/requirements/timeZoneAPIController/getTimeZoneProfileInitialSetup.md @@ -0,0 +1,20 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get Time Zone + +> ## Positive case + +1. ✅ Returns status code 200 and response data as follows: + i. current location + ii. timezone + +> ## Negative case + +1. ✅ Returns status code 400, if the token is missing in the request body. +2. ✅ Returns status code 403, if the no document exists in ProfileInitialSetupToken database with requested token. +3. ✅ Returns status code 400, if the location is missing. +4. ✅ Returns status code 404, if geocodeAPIEndpoint returns no results. +5. ✅ Returns status code 500, if any other error occurs. + +> ## Edge case diff --git a/src/controllers/emailController.js b/src/controllers/emailController.js index 13de952e3..15243ef5b 100644 --- a/src/controllers/emailController.js +++ b/src/controllers/emailController.js @@ -1,15 +1,87 @@ // emailController.js const jwt = require('jsonwebtoken'); +const cheerio = require('cheerio'); const emailSender = require('../utilities/emailSender'); +const { hasPermission } = require('../utilities/permissions'); const EmailSubcriptionList = require('../models/emailSubcriptionList'); const userProfile = require('../models/userProfile'); const frontEndUrl = process.env.FRONT_END_URL || 'http://localhost:3000'; const jwtSecret = process.env.JWT_SECRET || 'EmailSecret'; +const handleContentToOC = (htmlContent) => + ` + + + + + + ${htmlContent} + + `; + +const handleContentToNonOC = (htmlContent, email) => + ` + + + + + + ${htmlContent} +

      Thank you for subscribing to our email updates!

      +

      If you would like to unsubscribe, please click here

      + + `; + +function extractImagesAndCreateAttachments(html) { + const $ = cheerio.load(html); + const attachments = []; + + $('img').each((i, img) => { + const src = $(img).attr('src'); + if (src.startsWith('data:image')) { + const base64Data = src.split(',')[1]; + const _cid = `image-${i}`; + attachments.push({ + filename: `image-${i}.png`, + content: Buffer.from(base64Data, 'base64'), + cid: _cid, + }); + $(img).attr('src', `cid:${_cid}`); + } + }); + return { + html: $.html(), + attachments, + }; +} + const sendEmail = async (req, res) => { + const canSendEmail = await hasPermission(req.body.requestor, 'sendEmails'); + if (!canSendEmail) { + res.status(403).send('You are not authorized to send emails.'); + return; + } try { const { to, subject, html } = req.body; + // Validate required fields + if (!subject || !html || !to) { + const missingFields = []; + if (!subject) missingFields.push('Subject'); + if (!html) missingFields.push('HTML content'); + if (!to) missingFields.push('Recipient email'); + console.log('missingFields', missingFields); + return res + .status(400) + .send(`${missingFields.join(' and ')} ${missingFields.length > 1 ? 'are' : 'is'} required`); + } + + // Extract images and create attachments + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + + // Log recipient for debugging + console.log('Recipient:', to); + await emailSender(to, subject, html) .then(result => { @@ -21,6 +93,7 @@ const sendEmail = async (req, res) => { res.status(500).send('Error sending email'); }); + } catch (error) { console.error('Error sending email:', error); return res.status(500).send('Error sending email'); @@ -28,48 +101,51 @@ const sendEmail = async (req, res) => { }; const sendEmailToAll = async (req, res) => { + const canSendEmailToAll = await hasPermission(req.body.requestor, 'sendEmailToAll'); + if (!canSendEmailToAll) { + res.status(403).send('You are not authorized to send emails to all.'); + return; + } try { const { subject, html } = req.body; + if (!subject || !html) { + return res.status(400).send('Subject and HTML content are required'); + } + + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + const users = await userProfile.find({ - firstName: 'Haoji', + firstName: '', email: { $ne: null }, isActive: true, emailSubscriptions: true, }); - let to = ''; - const emailContent = ` - - - - + if (users.length === 0) { + return res.status(404).send('No users found'); + } + const recipientEmails = users.map((user) => user.email); + console.log('# sendEmailToAll to', recipientEmails.join(',')); + if (recipientEmails.length === 0) { + throw new Error('No recipients defined'); + } - - ${html} - - `; - users.forEach((user) => { - to += `${user.email},`; - }); - emailSender(to, subject, emailContent); - const emailList = await EmailSubcriptionList.find({ email: { $ne: null } }); - emailList.forEach((emailObject) => { - const { email } = emailObject; - const emailContent = ` - - - - + const emailContentToOCmembers = handleContentToOC(processedHtml); + await Promise.all( + recipientEmails.map((email) => + emailSender(email, subject, emailContentToOCmembers, attachments), + ), + ); + const emailSubscribers = await EmailSubcriptionList.find({ email: { $exists: true, $ne: '' } }); + console.log('# sendEmailToAll emailSubscribers', emailSubscribers.length); + await Promise.all( + emailSubscribers.map(({ email }) => { + const emailContentToNonOCmembers = handleContentToNonOC(processedHtml, email); + return emailSender(email, subject, emailContentToNonOCmembers, attachments); + }), + ); - - ${html} -

      Thank you for subscribing to our email updates!

      -

      If you would like to unsubscribe, please click here

      - - `; - emailSender(email, subject, emailContent); - }); return res.status(200).send('Email sent successfully'); } catch (error) { console.error('Error sending email:', error); @@ -107,13 +183,9 @@ const addNonHgnEmailSubscription = async (req, res) => { } const payload = { email }; - const token = jwt.sign( - payload, - jwtSecret, - { - expiresIn: 360, - }, - ); + const token = jwt.sign(payload, jwtSecret, { + expiresIn: 360, + }); const emailContent = ` diff --git a/src/controllers/emailController.spec.js b/src/controllers/emailController.spec.js new file mode 100644 index 000000000..f5327a328 --- /dev/null +++ b/src/controllers/emailController.spec.js @@ -0,0 +1,146 @@ +const { mockReq, mockRes, assertResMock } = require('../test'); +const emailController = require('./emailController'); +const jwt = require('jsonwebtoken'); +const userProfile = require('../models/userProfile'); + + +jest.mock('jsonwebtoken'); +jest.mock('../models/userProfile'); +jest.mock('../utilities/emailSender'); + + + + +const makeSut = () => { + const { + sendEmail, + sendEmailToAll, + updateEmailSubscriptions, + addNonHgnEmailSubscription, + removeNonHgnEmailSubscription, + confirmNonHgnEmailSubscription, + } = emailController; + return { + sendEmail, + sendEmailToAll, + updateEmailSubscriptions, + addNonHgnEmailSubscription, + removeNonHgnEmailSubscription, + confirmNonHgnEmailSubscription, + }; +}; +describe('emailController Controller Unit tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('sendEmail function', () => { + test('should send email successfully', async () => { + const { sendEmail } = makeSut(); + const mockReq = { + body: { + to: 'recipient@example.com', + subject: 'Test Subject', + html: '

      Test Body

      ', + }, + }; + const response = await sendEmail(mockReq, mockRes); + assertResMock(200, 'Email sent successfully', response, mockRes); + }); +}); + + describe('updateEmailSubscriptions function', () => { + test('should handle error when updating email subscriptions', async () => { + const { updateEmailSubscriptions } = makeSut(); + + + userProfile.findOneAndUpdate = jest.fn(); + + userProfile.findOneAndUpdate.mockRejectedValue(new Error('Update failed')); + + const mockReq = { + body: { + emailSubscriptions: ['subscription1', 'subscription2'], + requestor: { + email: 'test@example.com', + }, + }, + }; + + const response = await updateEmailSubscriptions(mockReq, mockRes); + + assertResMock(500, 'Error updating email subscriptions', response, mockRes); + }); + }); + + + describe('confirmNonHgnEmailSubscription function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeAll(() => { + jwt.verify = jest.fn(); + }); + + test('should return 400 if token is not provided', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + + const mockReq = { body: {} }; + const response = await confirmNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Invalid token', response, mockRes); + }); + + test('should return 401 if token is invalid', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: { token: 'invalidToken' } }; + + jwt.verify.mockImplementation(() => { + throw new Error('Token is not valid'); + }); + + await confirmNonHgnEmailSubscription(mockReq, mockRes); + + expect(mockRes.status).toHaveBeenCalledWith(401); + expect(mockRes.json).toHaveBeenCalledWith({ + errors: [ + { msg: 'Token is not valid' }, + ], + }); + }); + + + test('should return 400 if email is missing from payload', async () => { + const { confirmNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: { token: 'validToken' } }; + + // Mocking jwt.verify to return a payload without email + jwt.verify.mockReturnValue({}); + + const response = await confirmNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Invalid token', response, mockRes); + }); + + + + + + }); + describe('removeNonHgnEmailSubscription function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should return 400 if email is missing', async () => { + const { removeNonHgnEmailSubscription } = makeSut(); + const mockReq = { body: {} }; + + const response = await removeNonHgnEmailSubscription(mockReq, mockRes); + + assertResMock(400, 'Email is required', response, mockRes); + }); + }); + + }); diff --git a/src/controllers/informationController.js b/src/controllers/informationController.js index 792620995..03a23ba57 100644 --- a/src/controllers/informationController.js +++ b/src/controllers/informationController.js @@ -1,17 +1,17 @@ -const mongoose = require('mongoose'); +// const mongoose = require('mongoose'); // const userProfile = require('../models/userProfile'); // const hasPermission = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); -const cache = require('../utilities/nodeCache')(); +const cacheClosure = require('../utilities/nodeCache'); const informationController = function (Information) { + const cache = cacheClosure(); const getInformations = function (req, res) { // return all informations if cache is available if (cache.hasCache('informations')) { res.status(200).send(cache.getCache('informations')); return; } - Information.find({}, 'infoName infoContent visibility') .then((results) => { // cache results diff --git a/src/controllers/informationController.spec.js b/src/controllers/informationController.spec.js new file mode 100644 index 000000000..e69dd2a32 --- /dev/null +++ b/src/controllers/informationController.spec.js @@ -0,0 +1,392 @@ +/* eslint-disable no-unused-vars */ +// const mongoose = require('mongoose'); +const mongoose = require('mongoose'); + +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); +const Information = require('../models/information'); +const escapeRegex = require('../utilities/escapeRegex'); +const informationController = require('./informationController'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +/* eslint-disable no-unused-vars */ +/* eslint-disable prefer-promise-reject-errors */ + +const makeSut = () => { + const { addInformation, getInformations, updateInformation, deleteInformation } = + informationController(Information); + + return { + addInformation, + getInformations, + updateInformation, + deleteInformation, + }; +}; +// Define flushPromises function)); +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('informationController module', () => { + beforeEach(() => {}); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('addInformation function', () => { + test('Ensure addInformation returns 500 if any error when finding any information', async () => { + const { addInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + body: { + infoName: 'some infoName', + }, + }; + jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + assertResMock(500, { error: new Error('Error when finding') }, response, mockRes); + }); + test('Ensure addInformation returns 400 if duplicate info Name', async () => { + const { addInformation } = makeSut(); + const data = [{ infoName: 'test Info' }]; + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'test Info', + }, + }; + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + assertResMock( + 400, + { + error: `Info Name must be unique. Another infoName with name ${newMockReq.body.infoName} already exists. Please note that info names are case insensitive`, + }, + response, + mockRes, + ); + }); + test('Ensure addInformations returns 400 if any error when saving new Information', async () => { + const { addInformation } = makeSut(); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some Info', + }, + }; + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(true)); + jest + .spyOn(Information.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + assertResMock(400, new Error('Error when saving'), response, mockRes); + }); + + test('Ensure addInformation returns 201 if creating information successfully when no cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const { addInformation } = makeSut(); + const data = { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }; + + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve([])); + jest.spyOn(Information.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some addInfo', + infoContent: '1', + visibility: '1', + }, + }; + const response = addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(201, data, response, mockRes); + }); + test('Ensure addInformation returns 201 if creating information successfully', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id: 1}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const { addInformation } = makeSut(); + const data = [ + { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }, + ]; + + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve([])); + jest.spyOn(Information.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some addInfo', + infoContent: '1', + visibility: '1', + }, + }; + addInformation(newMockReq, mockRes); + await flushPromises(); + expect(findSpy).toHaveBeenCalledWith({ + infoName: { $regex: escapeRegex(newMockReq.body.infoName), $options: 'i' }, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); + describe('getInformations function', () => { + test('Ensure getInformations returns 200 if when informations key in cache', async () => { + const data = [ + { + _id: 1, + infoName: 'infoName', + infoContent: 'infoContent', + visibility: '1', + }, + ]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', data); + const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockImplementationOnce(() => data); + const { getInformations } = makeSut(); + + const response = getInformations(mockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(getCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, data, response, mockRes); + }); + test('Ensure getInformations returns 404 if any error when no informations key and catch error in finding', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding information'))); + const { getInformations } = makeSut(); + + const response = getInformations(mockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(findSpy).toHaveBeenCalledWith({}, 'infoName infoContent visibility'); + assertResMock(404, new Error('Error when finding information'), response, mockRes); + }); + + test('Ensure getInformations returns 200 when no informations key and no duplicated information', async () => { + const data = [ + { + infoName: 'mockAdd', + infoContent: 'mockContent', + visibility: '1', + }, + ]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', ''); + const findSpy = jest + .spyOn(Information, 'find') + .mockImplementationOnce(() => Promise.resolve(data)); + const setCacheMock = jest.spyOn(cacheObject, 'setCache').mockImplementationOnce(() => data); + + const { getInformations } = makeSut(); + const newMockReq = { + body: { + ...mockReq.body, + infoName: 'some getInfo', + infoContent: '1', + visibility: '1', + }, + }; + const response = getInformations(newMockReq, mockRes); + await flushPromises(); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(findSpy).toHaveBeenCalledWith({}, 'infoName infoContent visibility'); + expect(setCacheMock).toHaveBeenCalledWith('informations', data); + assertResMock(200, data, response, mockRes); + }); + }); + describe('deleteInformation function', () => { + test('Ensure deleteInformation returns 400 if any error when finding and delete information', async () => { + const errorMsg = 'Error when finding and deleting information by Id'; + const { deleteInformation } = makeSut(); + jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + const response = deleteInformation(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, new Error(errorMsg), response, mockRes); + }); + test('Ensure deleteInformation returns 200 if delete information successfully no cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const deletedData = { + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }; + const { deleteInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneDeleteSpy = jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.resolve(deletedData)); + const response = deleteInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneDeleteSpy).toHaveBeenCalledWith({ _id: deletedData.id }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, deletedData, response, mockRes); + }); + test('Ensure deleteInformation returns if delete information successfully and has cache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id:123}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const deletedData = { + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }; + const { deleteInformation } = makeSut(); + const newMockReq = { + ...mockReq.body, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + infoName: 'deletedInfo', + infoContent: 'deleted', + visibility: '1', + }, + }; + const findOneDeleteSpy = jest + .spyOn(Information, 'findOneAndDelete') + .mockImplementationOnce(() => Promise.resolve(deletedData)); + deleteInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneDeleteSpy).toHaveBeenCalledWith({ _id: deletedData.id }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); + describe('updateInformation function', () => { + test('Ensure updateInformation returns 400 if any error when finding and update information', async () => { + const errorMsg = 'Error when finding and updating information by Id'; + const { updateInformation } = makeSut(); + jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + const response = updateInformation(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, new Error(errorMsg), response, mockRes); + }); + test('Ensure updateInformation returns 200 if finding and update information successfuly when nocache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', ''); + const data = { + id: '601acda376045c7879d13a77', + infoName: 'updatedInfo', + infoContent: 'updated', + visibility: '1', + }; + const newMockReq = { + body: { + id: '601acda376045c7879d13a77', + infoName: 'oldInfo', + infoContent: 'old', + visibility: '0', + }, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneUpdateSpy = jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(data)); + const { updateInformation } = makeSut(); + const response = updateInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneUpdateSpy).toHaveBeenCalledWith({ _id: data.id }, newMockReq.body, { + new: true, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + assertResMock(200, data, response, mockRes); + }); + test('Ensure updateInformation returns if finding and update information successfuly when hascache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', '[{_id:123}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const data = { + id: '601acda376045c7879d13a77', + infoName: 'updatedInfo', + infoContent: 'updated', + visibility: '1', + }; + const newMockReq = { + body: { + id: '601acda376045c7879d13a77', + infoName: 'oldInfo', + infoContent: 'old', + visibility: '0', + }, + params: { + ...mockReq.params, + id: '601acda376045c7879d13a77', + }, + }; + const findOneUpdateSpy = jest + .spyOn(Information, 'findOneAndUpdate') + .mockImplementationOnce(() => Promise.resolve(data)); + const { updateInformation } = makeSut(); + updateInformation(newMockReq, mockRes); + await flushPromises(); + expect(findOneUpdateSpy).toHaveBeenCalledWith({ _id: data.id }, newMockReq.body, { + new: true, + }); + expect(hasCacheMock).toHaveBeenCalledWith('informations'); + expect(removeCacheMock).toHaveBeenCalledWith('informations'); + }); + }); +}); diff --git a/src/controllers/jobsController.js b/src/controllers/jobsController.js new file mode 100644 index 000000000..65cfeafdc --- /dev/null +++ b/src/controllers/jobsController.js @@ -0,0 +1,131 @@ +const Job = require('../models/jobs'); // Import the Job model + +// Controller to fetch all jobs with pagination, search, and filtering +const getJobs = async (req, res) => { + const { page = 1, limit = 18, search = '', category = '' } = req.query; + + try { + // Validate query parameters + const pageNumber = Math.max(1, parseInt(page, 10)); // Ensure page is at least 1 + const limitNumber = Math.max(1, parseInt(limit, 10)); // Ensure limit is at least 1 + + // Build query object + const query = {}; + if (search) query.title = { $regex: search, $options: 'i' }; // Case-insensitive search + if (category) query.category = category; + + // Fetch total count for pagination metadata + const totalJobs = await Job.countDocuments(query); + + // Fetch paginated results + const jobs = await Job.find(query) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + // Prepare response + res.json({ + jobs, + pagination: { + totalJobs, + totalPages: Math.ceil(totalJobs / limitNumber), + currentPage: pageNumber, + limit: limitNumber, + hasNextPage: pageNumber < Math.ceil(totalJobs / limitNumber), + hasPreviousPage: pageNumber > 1, + }, + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch jobs', details: error.message }); + } +}; + +// Controller to fetch job details by ID +const getJobById = async (req, res) => { + const { id } = req.params; + + try { + const job = await Job.findById(id); + if (!job) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(job); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch job', details: error.message }); + } +}; + +// Controller to create a new job +const createJob = async (req, res) => { + const { title, category, description, imageUrl, location, applyLink, jobDetailsLink } = req.body; + + try { + const newJob = new Job({ + title, + category, + description, + imageUrl, + location, + applyLink, + jobDetailsLink, + }); + + const savedJob = await newJob.save(); + res.status(201).json(savedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to create job', details: error.message }); + } +}; + +// Controller to update an existing job by ID +const updateJob = async (req, res) => { + const { id } = req.params; + + try { + const updatedJob = await Job.findByIdAndUpdate(id, req.body, { new: true }); + if (!updatedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(updatedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to update job', details: error.message }); + } +}; + +// Controller to delete a job by ID +const deleteJob = async (req, res) => { + const { id } = req.params; + + try { + const deletedJob = await Job.findByIdAndDelete(id); + if (!deletedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json({ message: 'Job deleted successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete job', details: error.message }); + } +}; + +const getCategories = async (req, res) => { + try { + const categories = await Job.distinct('category', {}); + + // Sort categories alphabetically + categories.sort((a, b) => a.localeCompare(b)); + + res.status(200).json({ categories }); + } catch (error) { + console.error('Error fetching categories:', error); + res.status(500).json({ message: 'Failed to fetch categories' }); + } +}; + +// Export controllers as a plain object +module.exports = { + getJobs, + getJobById, + createJob, + updateJob, + deleteJob, + getCategories, +}; diff --git a/src/controllers/logincontroller.js b/src/controllers/logincontroller.js index 3ba0203aa..794d00d70 100644 --- a/src/controllers/logincontroller.js +++ b/src/controllers/logincontroller.js @@ -63,7 +63,7 @@ const logincontroller = function () { res.status(200).send({ token }); } else { - res.status(403).send({ + res.status(404).send({ message: 'Invalid password.', }); } diff --git a/src/controllers/logincontroller.spec.js b/src/controllers/logincontroller.spec.js index 595bfe77b..995be69de 100644 --- a/src/controllers/logincontroller.spec.js +++ b/src/controllers/logincontroller.spec.js @@ -110,7 +110,7 @@ describe('logincontroller module', () => { expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); assertResMock( - 403, + 404, { message: 'Invalid password.', }, diff --git a/src/controllers/ownerMessageController.js b/src/controllers/ownerMessageController.js index 1b2c30205..3f74cb112 100644 --- a/src/controllers/ownerMessageController.js +++ b/src/controllers/ownerMessageController.js @@ -1,8 +1,11 @@ +const helper = require('../utilities/permissions'); + const ownerMessageController = function (OwnerMessage) { const getOwnerMessage = async function (req, res) { try { const results = await OwnerMessage.find({}); - if (results.length === 0) { // first time initialization + if (results.length === 0) { + // first time initialization const ownerMessage = new OwnerMessage(); await ownerMessage.save(); res.status(200).send({ ownerMessage }); @@ -15,7 +18,9 @@ const ownerMessageController = function (OwnerMessage) { }; const updateOwnerMessage = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if ( + !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) + ) { res.status(403).send('You are not authorized to create messages!'); } const { isStandard, newMessage } = req.body; @@ -40,7 +45,9 @@ const ownerMessageController = function (OwnerMessage) { }; const deleteOwnerMessage = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if ( + !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) + ) { res.status(403).send('You are not authorized to delete messages!'); } try { diff --git a/src/controllers/popupEditorController.spec.js b/src/controllers/popupEditorController.spec.js new file mode 100644 index 000000000..e70b05553 --- /dev/null +++ b/src/controllers/popupEditorController.spec.js @@ -0,0 +1,163 @@ +const PopUpEditor = require('../models/popupEditor'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +jest.mock('../utilities/permissions'); + +const helper = require('../utilities/permissions'); +const popupEditorController = require('./popupEditorController'); + +const flushPromises = () => new Promise(setImmediate); + +const mockHasPermission = (value) => + jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); + +const makeSut = () => { + const { getAllPopupEditors, getPopupEditorById, createPopupEditor, updatePopupEditor } = + popupEditorController(PopUpEditor); + return { getAllPopupEditors, getPopupEditorById, createPopupEditor, updatePopupEditor }; +}; + +describe('popupEditorController Controller Unit tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe(`getAllPopupEditors function`, () => { + test(`Should return 200 and popup editors on success`, async () => { + const { getAllPopupEditors } = makeSut(); + const mockPopupEditors = [{ popupName: 'popup', popupContent: 'content' }]; + jest.spyOn(PopUpEditor, 'find').mockResolvedValue(mockPopupEditors); + const response = await getAllPopupEditors(mockReq, mockRes); + assertResMock(200, mockPopupEditors, response, mockRes); + }); + + test(`Should return 404 on error`, async () => { + const { getAllPopupEditors } = makeSut(); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor, 'find').mockRejectedValue(error); + const response = await getAllPopupEditors(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + }); + }); + + describe(`getPopupEditorById function`, () => { + test(`Should return 200 and popup editor on success`, async () => { + const { getPopupEditorById } = makeSut(); + const mockPopupEditor = { popupName: 'popup', popupContent: 'content' }; + jest.spyOn(PopUpEditor, 'findById').mockResolvedValue(mockPopupEditor); + const response = await getPopupEditorById(mockReq, mockRes); + assertResMock(200, mockPopupEditor, response, mockRes); + }); + + test(`Should return 404 on error`, async () => { + const { getPopupEditorById } = makeSut(); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor, 'findById').mockRejectedValue(error); + const response = await getPopupEditorById(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + }); + }); + + describe(`createPopupEditor function`, () => { + test(`Should return 403 if user is not authorized`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(false); + const response = await createPopupEditor(mockReq, mockRes); + assertResMock( + 403, + { error: 'You are not authorized to create new popup' }, + response, + mockRes, + ); + }); + + test(`Should return 400 if popupName or popupContent is missing`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + const response = await createPopupEditor(mockReq, mockRes); + assertResMock( + 400, + { error: 'popupName , popupContent are mandatory fields' }, + response, + mockRes, + ); + }); + + test(`Should return 201 and popup editor on success`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + mockReq.body = { popupName: 'popup', popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockResolvedValue(mockReq.body) }; + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await createPopupEditor(mockReq, mockRes); + expect(mockPopupEditor.save).toHaveBeenCalled(); + assertResMock(201, mockReq.body, response, mockRes); + }); + + test(`Should return 500 on error`, async () => { + const { createPopupEditor } = makeSut(); + mockHasPermission(true); + const error = new Error('Test Error'); + + jest.spyOn(PopUpEditor.prototype, 'save').mockRejectedValue(error); + const response = await createPopupEditor(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, { error }, response, mockRes); + }); + }); + describe(`updatePopupEditor function`, () => { + test(`Should return 403 if user is not authorized`, async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(false); + const response = await updatePopupEditor(mockReq, mockRes); + assertResMock( + 403, + { error: 'You are not authorized to create new popup' }, + response, + mockRes, + ); + }); + + test(`Should return 400 if popupContent is missing`, async () => { + const { updatePopupEditor } = makeSut(); + mockReq.body = {}; + mockHasPermission(true); + const response = await updatePopupEditor(mockReq, mockRes); + assertResMock(400, { error: 'popupContent is mandatory field' }, response, mockRes); + }); + + test(`Should return 201 and popup editor on success`, async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(true); + mockReq.body = { popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockResolvedValue(mockReq.body) }; + jest.spyOn(PopUpEditor, 'findById').mockImplementationOnce((mockReq, callback) => callback(null, mockPopupEditor)); + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await updatePopupEditor(mockReq, mockRes); + expect(mockPopupEditor.save).toHaveBeenCalled(); + assertResMock(201, mockReq.body, response, mockRes); + }); + + test('Should return 500 on popupEditor save error', async () => { + const { updatePopupEditor } = makeSut(); + mockHasPermission(true); + const err = new Error('Test Error'); + mockReq.body = { popupContent: 'content' }; + const mockPopupEditor = { save: jest.fn().mockRejectedValue(err)}; + jest + .spyOn(PopUpEditor, 'findById') + .mockImplementation((mockReq, callback) => callback(null, mockPopupEditor)); + jest.spyOn(PopUpEditor.prototype, 'save').mockImplementationOnce(mockPopupEditor.save); + const response = await updatePopupEditor(mockReq, mockRes); + await flushPromises(); + assertResMock(500, {err}, response, mockRes); + }); + }); +}); diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index fcf24ce1a..e56e7e406 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -172,13 +172,22 @@ const profileInitialSetupController = function ( const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); - const acknowledgment = await sendEmailWithAcknowledgment( - email, - 'NEEDED: Complete your One Community profile setup', - sendLinkMessage(link), - ); - - return res.status(200).send(acknowledgment); + // Send response immediately without waiting for email acknowledgment + res.status(200).send({ message: 'Token created successfully, email is being sent.' }); + + // Asynchronously send the email acknowledgment + setImmediate(async () => { + try { + await sendEmailWithAcknowledgment( + email, + 'NEEDED: Complete your One Community profile setup', + sendLinkMessage(link), + ); + } catch (emailError) { + // Log email sending failure + LOGGER.logException(emailError, 'sendEmailWithAcknowledgment', JSON.stringify({ email, link }), null); + } + }); } catch (error) { await session.abortTransaction(); LOGGER.logException(error, 'getSetupToken', JSON.stringify(req.body), null); @@ -523,30 +532,26 @@ 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', - ); - } + + const { permissions } = req.body.requestor; + let user_permissions = ['getUserProfiles','postUserProfile','putUserProfile','changeUserStatus'] + if ((role === 'Administrator') || (role === 'Owner') || (role === 'Manager') || (role === 'Mentor') || user_permissions.some(e=>permissions.frontPermissions.includes(e))) { + 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.'); } diff --git a/src/controllers/reasonSchedulingController.spec.js b/src/controllers/reasonSchedulingController.spec.js new file mode 100644 index 000000000..e58c77466 --- /dev/null +++ b/src/controllers/reasonSchedulingController.spec.js @@ -0,0 +1,626 @@ +const moment = require('moment-timezone'); +const { mockReq, mockRes, mockUser } = require('../test'); +const UserModel = require('../models/userProfile'); + +jest.mock('../utilities/emailSender'); +const emailSender = require('../utilities/emailSender') + +const { + postReason, + getAllReasons, + getSingleReason, + patchReason, + deleteReason, +} = require('./reasonSchedulingController'); + +// assertResMock +const ReasonModel = require('../models/reason'); + +const flushPromises = () => new Promise(setImmediate); + +function mockDay(dayIdx, past = false) { + const date = moment().tz('America/Los_Angeles').startOf('day'); + while (date.day() !== dayIdx) { + date.add(past ? -1 : 1, 'days'); + } + return date; +} + +describe('reasonScheduling Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRes.json = jest.fn(); + mockReq.body = { + ...mockReq.body, + ...mockUser(), + reasonData: { + date: mockDay(0), + message: 'some reason', + }, + currentDate: moment.tz('America/Los_Angeles').startOf('day'), + }; + mockReq.params = { + ...mockReq.params, + ...mockUser(), + }; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + describe('postReason method', () => { + test('Ensure postReason returns 400 for warning to choose Sunday', async () => { + mockReq.body.reasonData.date = mockDay(1, true); + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: + "You must choose the Sunday YOU'LL RETURN as your date. This is so your reason ends up as a note on that blue square.", + errorCode: 0, + }), + ); + }); + test('Ensure postReason returns 400 for warning to choose a future date', async () => { + mockReq.body.reasonData.date = mockDay(0, true); + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You should select a date that is yet to come', + errorCode: 7, + }), + ); + }); + test('Ensure postReason returns 400 for not providing reason', async () => { + mockReq.body.reasonData.message = null; + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must provide a reason.', + errorCode: 6, + }), + ); + }); + test('Ensure postReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => + Promise.resolve(null)); + + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure postReason returns 403 when duplicate reason to the date', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + const mockReason = { + reason: 'Some Reason', + userId: mockReq.body.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(mockReason); + + await postReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'The reason must be unique to the date', + errorCode: 3, + }), + ); + }); + test('Ensure postReason returns 400 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + mockRes.sendStatus = jest.fn().mockReturnThis(); + const newReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(); + const mockSave = jest.spyOn(ReasonModel.prototype, 'save').mockRejectedValue(newReason); + emailSender.mockImplementation(() => { + throw new Error('Failed to send email'); + }); + + await postReason(mockReq, mockRes); + await flushPromises(); + emailSender.mockRejectedValue(new Error('Failed')); + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockSave).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + errMessage: 'Something went wrong', + }), + ); + }); + test('Ensure postReason returns 200 if schedule reason and send blue sqaure email successfully', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + jest.spyOn(UserModel, 'findOneAndUpdate').mockResolvedValueOnce({ + _id: mockReq.body.userId, + timeOffFrom: mockReq.body.currentDate, + timeOffTill: mockReq.body.reasonData.date, + }); + mockRes.sendStatus = jest.fn().mockReturnThis(); + const newReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(); + const mockSave = jest.spyOn(ReasonModel.prototype, 'save').mockResolvedValue(newReason); + emailSender.mockImplementation(() => { + Promise.resolve(); + }); + await postReason(mockReq, mockRes); + await flushPromises(); + expect(mockRes.sendStatus).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.body.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.body.userId, + }); + expect(mockSave).toHaveBeenCalledWith(); + }); + }); + describe('getAllReason method', () => { + test('Ensure get AllReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + }), + ); + }); + test('Ensure get AllReason returns 400 when any error in fetching the user', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const mockFoundReason = jest.spyOn(ReasonModel, 'find').mockRejectedValueOnce(null); + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + errMessage: 'Something went wrong while fetching the user', + }), + ); + }); + test('Ensure get AllReason returns 200 when get schedule reason successfully', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const reasons = { + reason: 'Some Reason', + userId: mockReq.params.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + isSet: true, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'find').mockResolvedValue(reasons); + await getAllReasons(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + reasons, + }), + ); + }); + }); + describe('getSingleReason method', () => { + test('Ensure getSingleReason return 400 when any error in fetching the user', async () => { + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Something went wrong while fetching single reason', + }), + ); + }); + test('Ensure getSingleReason return 404 when any error in find user by Id', async () => { + mockReq.query = { + queryData: mockDay(0), + }; + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure getSingleReason return 200 if not found schedule reason and return empty object successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + mockReq.query = { + queryDate: mockDay(0), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.query.queryDate, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith({ + reason: '', + date: '', + userId: '', + isSet: false, + }); + }); + test('Ensure getSingleReason return 200 if found schedule reason and return reason successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + mockReq.query = { + queryDate: mockDay(0), + }; + const singleReason = { + reason: 'Some Reason', + userId: mockReq.params.userId, + date: moment.tz('America/Los_Angeles').startOf('day').toISOString(), + isSet: true, + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValue(singleReason); + + await getSingleReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.query.queryDate, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith(singleReason); + }); + }); + describe('patchReason method', () => { + test('Ensure patchReason returns 400 for not providing reason', async () => { + mockReq.body.reasonData.message = null; + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must provide a reason.', + errorCode: 6, + }), + ); + }); + test('Ensure patchReason returns 404 when error in finding user Id', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure patchReason returns 404 when error in finding reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason not found', + errorCode: 4, + }), + ); + }); + test('Ensure patchReason returns 400 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const oldReason = { + reason: 'old message', + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + save: jest.fn().mockRejectedValueOnce(), + }; + emailSender.mockImplementation(() => { + throw new Error('Failed to send email'); + }); + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(oldReason); + await patchReason(mockReq, mockRes); + await flushPromises(); + emailSender.mockRejectedValue(new Error('Failed')); + expect(mockRes.status).toHaveBeenCalledWith(400); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(oldReason.save).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'something went wrong while patching the reason', + }), + ); + }); + test('Ensure patchReason returns 200 when any error in saving.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const oldReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + save: jest.fn().mockResolvedValueOnce(), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(oldReason); + emailSender.mockImplementation(() => { + Promise.resolve(); + }); + await patchReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + }); + expect(oldReason.save).toHaveBeenCalledWith(); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason Updated!', + }), + ); + }); + }); + describe('deleteReason method', () => { + test('Ensure deleteReason return 403 when no permission to delete', async () => { + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + ...mockReq.requestor, + requestor: { + role: 'Volunteer', + }, + }, + }; + await deleteReason(newMockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'You must be an Owner or Administrator to schedule a reason for a Blue Square', + + errorCode: 1, + }), + ); + }); + test('Ensure deleteReason return 404 when not finding user by ID', async () => { + const mockFindUser = jest.spyOn(UserModel, 'findById').mockImplementationOnce(() => null); + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'User not found', + errorCode: 2, + }), + ); + }); + test('Ensure deleteReason returns 404 when error in finding reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockResolvedValueOnce(); + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(404); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Reason not found', + errorCode: 4, + }), + ); + }); + test('Ensure deleteReason returns 500 when error in removing reason', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const foundReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + remove: jest.fn((cb) => cb(true)), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockReturnValueOnce(foundReason); + + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Error while deleting document', + errorCode: 5, + }), + ); + }); + test('Ensure deleteReason returns 200 if delete reason successfully.', async () => { + const mockFindUser = jest + .spyOn(UserModel, 'findById') + .mockImplementationOnce(() => mockUser()); + const foundReason = { + reason: mockReq.body.reasonData.message, + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + userId: mockReq.params.userId, + remove: jest.fn((cb) => cb(false)), + }; + const mockFoundReason = jest.spyOn(ReasonModel, 'findOne').mockReturnValueOnce(foundReason); + + await deleteReason(mockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockFindUser).toHaveBeenCalledWith(mockReq.params.userId); + expect(mockFoundReason).toHaveBeenCalledWith({ + date: moment + .tz(mockReq.body.reasonData.date, 'America/Los_Angeles') + .startOf('day') + .toISOString(), + }); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Document deleted', + }), + ); + }); + }); +}); diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 1e019ffa1..b4d71a5dd 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -683,7 +683,10 @@ const taskController = function (Task) { }; const updateTask = async (req, res) => { - if (!(await hasPermission(req.body.requestor, 'updateTask'))) { + if ( + !(await hasPermission(req.body.requestor, 'updateTask')) && + !(await hasPermission(req.body.requestor, 'removeUserFromTask')) + ) { res.status(403).send({ error: 'You are not authorized to update Task.' }); return; } diff --git a/src/controllers/taskController.spec.js b/src/controllers/taskController.spec.js new file mode 100644 index 000000000..7d1196df7 --- /dev/null +++ b/src/controllers/taskController.spec.js @@ -0,0 +1,1555 @@ +const mongoose = require('mongoose'); + +// Utility to aid in testing +jest.mock('../utilities/permissions', () => ({ + hasPermission: jest.fn(), +})); + +jest.mock('../utilities/emailSender', () => jest.fn()); + +const taskHelperMethods = { + getTasksForTeams: jest.fn(), + getTasksForSingleUser: jest.fn(), +}; +jest.mock('../helpers/taskHelper', () => () => ({ ...taskHelperMethods })); + +const flushPromises = () => new Promise(setImmediate); +const { mockReq, mockRes, assertResMock } = require('../test'); +const { hasPermission } = require('../utilities/permissions'); +const emailSender = require('../utilities/emailSender'); + +// controller to test +const taskController = require('./taskController'); + +// MongoDB Model imports +const Task = require('../models/task'); +const Project = require('../models/project'); +const UserProfile = require('../models/userProfile'); +const WBS = require('../models/wbs'); +const FollowUp = require('../models/followUp'); + +const makeSut = () => { + const { + getTasks, + getWBSId, + importTask, + postTask, + updateNum, + moveTask, + deleteTask, + deleteTaskByWBS, + updateTask, + swap, + getTaskById, + fixTasks, + updateAllParents, + getTasksByUserId, + sendReviewReq, + getTasksForTeamsByUser, + updateTaskStatus, + } = taskController(Task); + + return { + getTasks, + getWBSId, + importTask, + postTask, + updateNum, + moveTask, + deleteTask, + deleteTaskByWBS, + updateTask, + swap, + getTaskById, + fixTasks, + updateAllParents, + getTasksByUserId, + sendReviewReq, + getTasksForTeamsByUser, + updateTaskStatus, + }; +}; + +describe('Unit Tests for taskController.js', () => { + afterAll(() => { + jest.resetAllMocks(); + }); + + describe('getTasks function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns 200 on successfully querying the document', async () => { + const { getTasks } = makeSut(); + const mockData = 'some random data'; + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce(mockData); + + const response = await getTasks(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 200 on successfully querying the document', async () => { + const { getTasks } = makeSut(); + const error = 'some random error'; + + const taskFindSpy = jest.spyOn(Task, 'find').mockRejectedValueOnce(error); + + const response = await getTasks(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getWBSId function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns 200 on successfully querying the document', async () => { + const { getWBSId } = makeSut(); + const mockData = 'some random data'; + + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValueOnce(mockData); + + const response = await getWBSId(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalledTimes(1); + }); + + test('Returns 200 on successfully querying the document', async () => { + const { getWBSId } = makeSut(); + const error = 'some random error'; + + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockRejectedValueOnce(error); + + const response = await getWBSId(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('importTasks function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `importTask` permission is missing', async () => { + const { importTask } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to create new Task.' }; + + const response = await importTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 201 on successful import operation', async () => { + const { importTask } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.params.wbs = 'wbs123'; + mockReq.body.list = [ + { + _id: 'mongoDB-Id', + num: '1', + level: 1, + parentId1: null, + parentId2: null, + parentId3: null, + mother: null, + resources: ['parth|userId123|parthProfilePic', 'test|test123|testProfilePic'], + }, + ]; + + const saveMock = jest + .fn() + .mockImplementation(() => Promise.resolve({ _id: '1', wbsId: 'wbs123' })); + const TaskConstructorSpy = jest.spyOn(Task.prototype, 'save').mockImplementation(saveMock); + + const data = 'done'; + + const response = await importTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(201, data, response, mockRes); + expect(TaskConstructorSpy).toBeCalled(); + }); + + test('Return 400 on encountering any error while saving task', async () => { + const { importTask } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.params.wbs = 'wbs123'; + mockReq.body.list = [ + { + _id: 'mongoDB-Id', + num: '1', + level: 1, + parentId1: null, + parentId2: null, + parentId3: null, + mother: null, + resources: ['parth|userId123|parthProfilePic', 'test|test123|testProfilePic'], + }, + ]; + + const error = new Error('error while saving'); + + const saveMock = jest.fn().mockImplementation(() => Promise.reject(error)); + const TaskConstructorSpy = jest.spyOn(Task.prototype, 'save').mockImplementation(saveMock); + + const response = await importTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(TaskConstructorSpy).toBeCalled(); + }); + }); + + describe('postTask function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `postTask` permission is missing', async () => { + const { postTask } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to create new Task.' }; + + const response = await postTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test.each([ + [ + { taskName: undefined, isActive: true }, + 'Task Name, Active status, Task Number are mandatory fields', + ], + [ + { taskName: 'some task name', isActive: undefined }, + 'Task Name, Active status, Task Number are mandatory fields', + ], + [ + { taskName: undefined, isActive: undefined }, + 'Task Name, Active status, Task Number are mandatory fields', + ], + ])('Return 400 if any required field is missing', async (body, expectedError) => { + const { postTask } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + // Set the request body based on the current test case + mockReq.body.taskName = body.taskName; + mockReq.body.isActive = body.isActive; + + const error = { error: expectedError }; + + const response = await postTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 201 on successfully saving a new Task', async () => { + const { postTask } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + const newTask = { + taskName: 'Sample Task', + wbsId: new mongoose.Types.ObjectId(), + num: '1', + level: 1, + position: 1, + childrenQty: 0, + isActive: true, + }; + + // Mock the current datetime + const currentDate = Date.now(); + + // Mock Task model + const mockTask = { + save: jest.fn().mockResolvedValue({ + _id: new mongoose.Types.ObjectId(), + wbsId: new mongoose.Types.ObjectId(), + createdDatetime: currentDate, + modifiedDatetime: currentDate, + }), + }; + const taskSaveSpy = jest.spyOn(Task.prototype, 'save').mockResolvedValue(mockTask); + + // Mock WBS model + const mockWBS = { + _id: new mongoose.Types.ObjectId(), + projectId: 'projectId', + modifiedDatetime: Date.now(), + save: jest.fn().mockResolvedValue({ + _id: new mongoose.Types.ObjectId(), + projectId: 'projectId', + modifiedDatetime: Date.now(), + }), + }; + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockWBS); + + // Mock Project model + const mockProjectObj = { + save: jest.fn().mockResolvedValue({ + _id: new mongoose.Types.ObjectId(), + modifiedDatetime: currentDate, + }), + modifiedDatetime: currentDate, + }; + const projectFindByIdSpy = jest.spyOn(Project, 'findById').mockResolvedValue(mockProjectObj); + + // add the necessary request params + mockReq.params = { + ...mockReq.params, + id: new mongoose.Types.ObjectId(), + }; + + // add the necessary body parameters + mockReq.body = { + ...mockReq.body, + ...newTask, + }; + + const response = await postTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(201, expect.anything(), response, mockRes); + expect(taskSaveSpy).toBeCalled(); + expect(wbsFindByIdSpy).toBeCalled(); + expect(projectFindByIdSpy).toBeCalled(); + }); + + test('Return 400 on encountering any error during Promise.all', async () => { + const { postTask } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + const newTask = { + taskName: 'Sample Task', + wbsId: new mongoose.Types.ObjectId(), + num: '1', + level: 1, + position: 1, + childrenQty: 0, + isActive: true, + }; + + // Mock the current datetime + const currentDate = Date.now(); + + // Mock the Task model + const mockTaskError = new Error('Failed to save task'); + + // Use jest.fn() to mock the save method to reject with an error + const taskSaveMock = jest.fn().mockRejectedValue(mockTaskError); + + // Spy on the Task prototype's save method + const taskSaveSpy = jest.spyOn(Task.prototype, 'save').mockImplementation(taskSaveMock); + + // Mock WBS model + const mockWBS = { + _id: new mongoose.Types.ObjectId(), + projectId: 'projectId', + modifiedDatetime: Date.now(), + save: jest.fn().mockResolvedValue({ + _id: new mongoose.Types.ObjectId(), + projectId: 'projectId', + modifiedDatetime: Date.now(), + }), + }; + // Mock `WBS.findById` to return `mockWBS` + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockWBS); + + // Mock Project model + const mockProjectObj = { + save: jest.fn().mockResolvedValueOnce({ + _id: new mongoose.Types.ObjectId(), + modifiedDatetime: currentDate, + }), + modifiedDatetime: currentDate, + }; + const projectFindByIdSpy = jest + .spyOn(Project, 'findById') + .mockResolvedValueOnce(mockProjectObj); + + // add the necessary request params + mockReq.params = { + ...mockReq.params, + id: new mongoose.Types.ObjectId(), + }; + + // add the necessary body parameters + mockReq.body = { + ...mockReq.body, + ...newTask, + }; + + const response = await postTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, mockTaskError, response, mockRes); + expect(taskSaveSpy).toBeCalled(); + expect(wbsFindByIdSpy).toBeCalled(); + expect(projectFindByIdSpy).toBeCalled(); + }); + }); + + describe('updateNum function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `updateNum` permission is missing', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to create new projects.' }; + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 400 if `nums` is missing from the request body', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + const error = { error: 'Num is a mandatory fields' }; + mockReq.body.nums = null; + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 200 on successful update - nums is empty array', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.nums = []; + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, true, response, mockRes); + }); + + test('Return 200 on successful update - nums is not an empty array', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.nums = [ + { + id: 'sample-id', + num: 'sample-num', + }, + ]; + + const mockDataForTaskFindByIdSpy = { + num: 0, + save: jest.fn().mockResolvedValue({}), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + callback(null, mockDataForTaskFindByIdSpy); + }); + + const mockDataForTaskFindSpy = []; + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce(mockDataForTaskFindSpy); + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, true, response, mockRes); + expect(taskFindSpy).toBeCalled(); + expect(taskFindByIdSpy).toBeCalled(); + }); + + test('Return 404 if error occurs on Task.find()', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.nums = [ + { + id: 'sample-id', + num: 'sample-num', + }, + ]; + + const mockDataForTaskFindByIdSpy = { + num: 0, + save: jest.fn().mockResolvedValue({}), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + callback(null, mockDataForTaskFindByIdSpy); + }); + + const mockError = new Error({ error: 'some error occurred' }); + const taskFindSpy = jest.spyOn(Task, 'find').mockRejectedValueOnce(mockError); + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, mockError, response, mockRes); + expect(taskFindSpy).toBeCalled(); + expect(taskFindByIdSpy).toBeCalled(); + }); + + test('Return 400 if error occurs while saving a Task within Task.findById()', async () => { + const { updateNum } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.nums = [ + { + id: 'sample-id', + num: 'sample-num', + }, + ]; + + const mockError = new Error({ error: 'some error occurred' }); + + const mockDataForTaskFindByIdSpy = { + num: 0, + save: jest.fn().mockRejectedValueOnce(mockError), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + callback(null, mockDataForTaskFindByIdSpy); + }); + + const mockDataForTaskFindSpy = []; + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce(mockDataForTaskFindSpy); + + const response = await updateNum(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, mockError, response, mockRes); + expect(taskFindSpy).toBeCalled(); + expect(taskFindByIdSpy).toBeCalled(); + }); + }); + + describe('moveTask function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 400 if either `fromNum` or `toNum` is missing in request body', async () => { + const { moveTask } = makeSut(); + + const error = { error: 'wbsId, fromNum, toNum are mandatory fields' }; + mockReq.body.fromNum = null; + + const response = await moveTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 200 on successful exeecution', async () => { + const { moveTask } = makeSut(); + + const requestData = { + body: { + fromNum: '1.0', + toNum: '2.0', + }, + }; + + mockReq.body = { + ...mockReq.body, + ...requestData.body, + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValue([ + { num: '1.0', save: jest.fn().mockResolvedValue({}) }, + { num: '1.1', save: jest.fn().mockResolvedValue({}) }, + ]); + + mockReq.params.wbsId = 'someWbsId'; + + const response = await moveTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, 'Success!', response, mockRes); + expect(taskFindSpy).toBeCalled(); + }); + + test('Return 400 on some error', async () => { + const { moveTask } = makeSut(); + + const requestData = { + body: { + fromNum: '1.0', + toNum: '2.0', + }, + }; + + mockReq.body = { + ...mockReq.body, + ...requestData.body, + }; + + const error = new Error({ error: 'some error' }); + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValue([ + { num: '1.0', save: jest.fn().mockResolvedValue({}) }, + { num: '1.1', save: jest.fn().mockRejectedValueOnce(error) }, + ]); + + mockReq.params.wbsId = 'someWbsId'; + + const response = await moveTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindSpy).toBeCalled(); + }); + }); + + describe('deleteTask function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `deleteTask` permission is missing', async () => { + const { deleteTask } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to deleteTasks.' }; + + const response = await deleteTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 400 if no Task found', async () => { + const { deleteTask } = makeSut(); + + const error = { error: 'No valid records found' }; + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + mother: 'null', + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValue([]); + const followUpFindOneAndDeleteSpy = jest + .spyOn(FollowUp, 'findOneAndDelete') + .mockResolvedValue(true); + + const response = await deleteTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + expect(followUpFindOneAndDeleteSpy).toHaveBeenCalled(); + }); + + test('Return 200 on successfully deleting task', async () => { + const { deleteTask } = makeSut(); + + const message = { message: 'Task successfully deleted' }; + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + mother: 'null', + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValue([ + { + remove: jest.fn().mockImplementation(() => Promise.resolve(1)), + }, + ]); + const followUpFindOneAndDeleteSpy = jest + .spyOn(FollowUp, 'findOneAndDelete') + .mockResolvedValue(true); + + const response = await deleteTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, message, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + expect(followUpFindOneAndDeleteSpy).toHaveBeenCalled(); + }); + }); + + describe('deleteTaskByWBS function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `deleteTask` permission is missing', async () => { + const { deleteTaskByWBS } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to deleteTasks.' }; + + const response = await deleteTaskByWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 400 if no Task found', async () => { + const { deleteTaskByWBS } = makeSut(); + + const error = { error: 'No valid records found' }; + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + wbsId: 456, + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockImplementation((query, callback) => { + callback(null, []); + return { + catch: jest.fn(), + }; + }); + + const response = await deleteTaskByWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + }); + + test('Return 400 if Task.find fails', async () => { + const { deleteTaskByWBS } = makeSut(); + + const expectedError = new Error('Database error'); + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + wbsId: 456, + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockImplementation((query, callback) => { + callback(expectedError, null); + return { + catch: jest.fn((catchCallback) => { + catchCallback(expectedError); + return Promise.resolve(); + }), + }; + }); + + const response = await deleteTaskByWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, expectedError, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + }); + + test('Return 200 on successfully deleting task', async () => { + const { deleteTaskByWBS } = makeSut(); + + const message = { message: ' Tasks were successfully deleted' }; + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + wbsId: 456, + }; + + const taskFindSpy = jest.spyOn(Task, 'find').mockImplementation((query, callback) => { + callback(null, [ + { + remove: jest.fn().mockImplementation(() => Promise.resolve(1)), + }, + ]); + return { + catch: jest.fn(), + }; + }); + + const followUpFindOneAndDeleteSpy = jest + .spyOn(FollowUp, 'findOneAndDelete') + .mockResolvedValue(true); + + const response = await deleteTaskByWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, message, response, mockRes); + expect(taskFindSpy).toHaveBeenCalled(); + expect(followUpFindOneAndDeleteSpy).toHaveBeenCalled(); + }); + }); + + describe('updateTask function()', () => { + const mockedTask = { + wbs: 111, + }; + const mockedWBS = { + projectId: 111, + modifiedDatetime: new Date(), + save: jest.fn(), + }; + const mockedProject = { + projectId: 111, + modifiedDatetime: new Date(), + save: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `updateTask` permission is missing', async () => { + const { updateTask } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to update Task.' }; + + const response = await updateTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 200 on successful update', async () => { + const { updateTask } = makeSut(); + + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValue(mockedTask); + const taskFindOneAndUpdateSpy = jest + .spyOn(Task, 'findOneAndUpdate') + .mockResolvedValueOnce(true); + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockedWBS); + const projectFindByIdSpy = jest.spyOn(Project, 'findById').mockResolvedValue(mockedProject); + + const response = await updateTask(mockReq, mockRes); + await flushPromises(); + + // assertResMock(201, null, response, mockRes); + expect(mockRes.status).toBeCalledWith(201); + expect(response).toBeUndefined(); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindOneAndUpdateSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(projectFindByIdSpy).toHaveBeenCalled(); + }); + + test('Return 404 on encountering error', async () => { + const { updateTask } = makeSut(); + + const error = { error: 'No valid records found' }; + hasPermission.mockResolvedValueOnce(true); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValue(mockedTask); + const taskFindOneAndUpdateSpy = jest + .spyOn(Task, 'findOneAndUpdate') + .mockRejectedValueOnce(error); + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockedWBS); + const projectFindByIdSpy = jest.spyOn(Project, 'findById').mockResolvedValue(mockedProject); + + const response = await updateTask(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindOneAndUpdateSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(projectFindByIdSpy).toHaveBeenCalled(); + }); + }); + + describe('swap function()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Return 403 if `swapTask` permission is missing', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(false); + + const error = { error: 'You are not authorized to create new projects.' }; + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, error, response, mockRes); + }); + + test('Return 400 if `taskId1` is missing', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = null; + mockReq.body.taskId2 = 'some-value'; + + const error = { error: 'taskId1 and taskId2 are mandatory fields' }; + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 400 if `taskId2` is missing', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'some-value'; + mockReq.body.taskId2 = null; + + const error = { error: 'taskId1 and taskId2 are mandatory fields' }; + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 400 if `taskId1` and `taskId2` are missing', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = null; + mockReq.body.taskId2 = null; + + const error = { error: 'taskId1 and taskId2 are mandatory fields' }; + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Return 400 if no task exists with the id same as `taskId1`', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'invalid-taskId1'; + mockReq.body.taskId2 = 'some value'; + + const error = 'No valid records found'; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'invalid-taskId1') { + callback(null, null); // the first null shows no error | second null show no task1 + } else if (id === 'invalid-taskId2') { + callback(null, 'some task2 exists'); + } + }); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + }); + + test('Return 400 if no task exists with the id same as `taskId2`', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'valid-taskId1'; + mockReq.body.taskId2 = 'invalid-taskId2'; + + const error = 'No valid records found'; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'valid-taskId1') { + callback(null, { _id: 'valid-taskId1', name: 'Task 1' }); + } + + if (id === 'invalid-taskId2') { + callback(null, null); // the first null shows no error | second null show no task2 found + } + }); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalledTimes(2); + expect(taskFindByIdSpy).toHaveBeenNthCalledWith(1, 'valid-taskId1', expect.any(Function)); + expect(taskFindByIdSpy).toHaveBeenNthCalledWith(2, 'invalid-taskId2', expect.any(Function)); + }); + + test('Return 400 if some error occurs while saving task1', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'valid-taskId1'; + mockReq.body.taskId2 = 'valid-taskId2'; + + const error = 'some error'; + + const validTask1 = { + _id: 'valid-taskId1', + name: 'Task 1', + num: 1, + parentId: 'pId', + save: jest.fn().mockRejectedValue(error), + }; + + const validTask2 = { + _id: 'valid-taskId2', + name: 'Task 2', + num: 2, + parentId: 'pId', + save: jest.fn().mockResolvedValue('sadasd'), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'valid-taskId1') { + callback(null, validTask1); + } + if (id === 'valid-taskId2') { + callback(null, validTask2); + } + }); + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce('works fine'); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalled(); + }); + + test('Return 400 if some error occurs while saving task2', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'valid-taskId1'; + mockReq.body.taskId2 = 'valid-taskId2'; + + const error = 'some error'; + + const validTask1 = { + _id: 'valid-taskId1', + name: 'Task 1', + num: 1, + parentId: 'pId', + save: jest.fn().mockResolvedValue(), + }; + + const validTask2 = { + _id: 'valid-taskId2', + name: 'Task 2', + num: 2, + parentId: 'pId', + save: jest.fn().mockRejectedValue(error), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'valid-taskId1') { + callback(null, validTask1); + } + if (id === 'valid-taskId2') { + callback(null, validTask2); + } + }); + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce('works fine'); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalled(); + }); + + test('Return 404 if some error occurs while saving task.find', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'valid-taskId1'; + mockReq.body.taskId2 = 'valid-taskId2'; + + const error = 'some error'; + + const validTask1 = { + _id: 'valid-taskId1', + name: 'Task 1', + num: 1, + parentId: 'pId', + save: jest.fn().mockResolvedValue(), + }; + + const validTask2 = { + _id: 'valid-taskId2', + name: 'Task 2', + num: 2, + parentId: 'pId', + save: jest.fn().mockResolvedValue(), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'valid-taskId1') { + callback(null, validTask1); + } + if (id === 'valid-taskId2') { + callback(null, validTask2); + } + }); + + const taskFindSpy = jest.spyOn(Task, 'find').mockRejectedValueOnce(error); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalled(); + }); + + test('Return 200 if swapped correctly', async () => { + const { swap } = makeSut(); + hasPermission.mockResolvedValueOnce(true); + + mockReq.body.taskId1 = 'valid-taskId1'; + mockReq.body.taskId2 = 'valid-taskId2'; + + const message = 'no error'; + + const validTask1 = { + _id: 'valid-taskId1', + name: 'Task 1', + num: 1, + parentId: 'pId', + save: jest.fn().mockResolvedValue(), + }; + + const validTask2 = { + _id: 'valid-taskId2', + name: 'Task 2', + num: 2, + parentId: 'pId', + save: jest.fn().mockResolvedValue(), + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockImplementation((id, callback) => { + if (id === 'valid-taskId1') { + callback(null, validTask1); + } + if (id === 'valid-taskId2') { + callback(null, validTask2); + } + }); + + const taskFindSpy = jest.spyOn(Task, 'find').mockResolvedValueOnce(message); + + const response = await swap(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, message, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindSpy).toHaveBeenCalled(); + }); + }); + + describe('getTaskById function()', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns 400 if the taskId is missing from the params', async () => { + const { getTaskById } = makeSut(); + + mockReq.params.id = null; + + const error = { error: 'Task ID is missing' }; + + const response = await getTaskById(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + }); + + test('Returns 400 if the taskId is missing from the params', async () => { + const { getTaskById } = makeSut(); + + mockReq.params.id = 'someTaskId'; + + const error = { error: 'This is not a valid task' }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValueOnce(null); + + const response = await getTaskById(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + }); + + test('Returns 500 if some error occurs at Task.findById', async () => { + const { getTaskById } = makeSut(); + + mockReq.params.id = 'someTaskId'; + + const error = new Error('some error occurred'); + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockRejectedValueOnce(error); + + const response = await getTaskById(mockReq, mockRes); + await flushPromises(); + + assertResMock( + 500, + { error: 'Internal Server Error', details: error.message }, + response, + mockRes, + ); + expect(taskFindByIdSpy).toHaveBeenCalled(); + }); + + test('Returns 200 if some error occurs at Task.findById', async () => { + const { getTaskById } = makeSut(); + + mockReq.params.id = 'someTaskId'; + + const mockTask = { + resources: [], + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValueOnce(mockTask); + + const response = await getTaskById(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockTask, response, mockRes); + expect(taskFindByIdSpy).toHaveBeenCalled(); + }); + }); + + describe('fixTasks function()', () => { + test('Returns 200 without performing any action', async () => { + const { fixTasks } = makeSut(); + + const response = fixTasks(mockReq, mockRes); + + await flushPromises(); + + assertResMock(200, 'done', response, mockRes); + }); + }); + + describe('updateAllParents function()', () => { + test('Returns 200 Task.Find() on successful operation', async () => { + const { updateAllParents } = makeSut(); + + const mockTasks = []; + + const taskFind = jest.spyOn(Task, 'find').mockResolvedValueOnce(mockTasks); + const response = updateAllParents(mockReq, mockRes); + + await flushPromises(); + + assertResMock(200, 'done', response, mockRes); + expect(taskFind).toHaveBeenCalled(); + }); + + test('Returns 400 on some error', async () => { + const { updateAllParents } = makeSut(); + + const error = new Error('some error'); + + const taskFind = jest.spyOn(Task, 'find').mockImplementationOnce(() => { + throw error; + }); + const response = await updateAllParents(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, error, response, mockRes); + expect(taskFind).toHaveBeenCalled(); + }); + }); + + describe('getTasksByUserId function()', () => { + test('Returns 200 and tasks when aggregation is successful', async () => { + const { getTasksByUserId } = makeSut(); + + mockReq.params.userId = '507f1f77bcf86cd799439011'; + + const mockTasks = [ + { _id: 'task1', taskName: 'Task 1', wbsName: 'WBS 1', projectName: 'Project 1' }, + { _id: 'task2', taskName: 'Task 2', wbsName: 'WBS 2', projectName: 'Project 2' }, + ]; + + // Mock the Task.aggregate method + const mockAggregate = { + match: jest.fn().mockReturnThis(), + lookup: jest.fn().mockReturnThis(), + unwind: jest.fn().mockReturnThis(), + addFields: jest.fn().mockReturnThis(), + project: jest.fn().mockReturnThis(), + }; + + mockAggregate.project.mockResolvedValue(mockTasks); + + const taskAggregate = jest.spyOn(Task, 'aggregate').mockReturnValue(mockAggregate); + + const response = await getTasksByUserId(mockReq, mockRes); + + assertResMock(200, mockTasks, response, mockRes); + expect(taskAggregate).toHaveBeenCalled(); + }); + + test('Returns 400 when error occurs', async () => { + const { getTasksByUserId } = makeSut(); + + mockReq.params.userId = '507f1f77bcf86cd799439011'; + + const mockError = new Error('some error'); + + // Mock the Task.aggregate method + const mockAggregate = { + match: jest.fn().mockReturnThis(), + lookup: jest.fn().mockReturnThis(), + unwind: jest.fn().mockReturnThis(), + addFields: jest.fn().mockReturnThis(), + project: jest.fn().mockReturnThis(), + }; + + mockAggregate.project.mockRejectedValueOnce(mockError); + + const taskAggregate = jest.spyOn(Task, 'aggregate').mockReturnValue(mockAggregate); + + const response = await getTasksByUserId(mockReq, mockRes); + + assertResMock(400, mockError, response, mockRes); + expect(taskAggregate).toHaveBeenCalled(); + }); + }); + + describe('sendReviewReq function()', () => { + test('Returns 200 on success', async () => { + const { sendReviewReq } = makeSut(); + + mockReq.body = { + ...mockReq.body, + myUserId: 'id', + name: 'name', + taskName: 'task', + }; + + const userProfileFindByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValueOnce([]); + const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce([]); + + const response = await sendReviewReq(mockReq, mockRes); + + assertResMock(200, 'Success', response, mockRes); + expect(emailSender).toHaveBeenCalledWith( + [], + expect.any(String), + expect.any(String), + null, + null, + ); + expect(userProfileFindByIdSpy).toHaveBeenCalled(); + expect(userProfileFindSpy).toHaveBeenCalled(); + }); + + test('Returns 400 on error', async () => { + const { sendReviewReq } = makeSut(); + + mockReq.body = { + ...mockReq.body, + myUserId: 'id', + name: 'name', + taskName: 'task', + }; + + const mockError = new Error('some error'); + + emailSender.mockImplementation(() => mockError); + + const userProfileFindByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValueOnce([]); + const userProfileFindSpy = jest.spyOn(UserProfile, 'find').mockResolvedValueOnce([]); + + const response = await sendReviewReq(mockReq, mockRes); + + assertResMock(400, mockError, response, mockRes); + + expect(emailSender).toHaveBeenCalledWith( + [], + expect.any(String), + expect.any(String), + null, + null, + ); + expect(userProfileFindByIdSpy).toHaveBeenCalled(); + expect(userProfileFindSpy).toHaveBeenCalled(); + }); + }); + + describe('getTasksForTeamsByUser function()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('Returns 200 on success - getTasksForTeams', async () => { + mockReq.params.userId = 1234; + const mockData = ['mockData']; + + taskHelperMethods.getTasksForTeams.mockResolvedValueOnce(mockData); + + const { getTasksForTeamsByUser } = makeSut(); + + const response = await getTasksForTeamsByUser(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + }); + + test('Returns 200 on success - getTasksForTeamsByUser', async () => { + mockReq.params.userId = 1234; + const mockData = ['mockData']; + + const execMock = { + exec: jest.fn().mockResolvedValueOnce(mockData), + }; + + taskHelperMethods.getTasksForTeams.mockResolvedValueOnce([]); + taskHelperMethods.getTasksForSingleUser.mockImplementation(() => execMock); + + const { getTasksForTeamsByUser } = makeSut(); + + const response = await getTasksForTeamsByUser(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, mockData, response, mockRes); + }); + + test('Returns 400 on error', async () => { + mockReq.params.userId = 1234; + const mockError = new Error('error'); + + taskHelperMethods.getTasksForTeams.mockRejectedValueOnce(mockError); + + const { getTasksForTeamsByUser } = makeSut(); + + const response = await getTasksForTeamsByUser(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, { error: mockError }, response, mockRes); + }); + }); + + describe('updateTaskStatus function()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockedTask = { + wbs: 111, + }; + const mockedWBS = { + projectId: 111, + modifiedDatetime: new Date(), + save: jest.fn(), + }; + const mockedProject = { + projectId: 111, + modifiedDatetime: new Date(), + save: jest.fn(), + }; + + test('Returns 200 on success - updateTaskStatus', async () => { + const { updateTaskStatus } = makeSut(); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValue(mockedTask); + const taskFindOneAndUpdateSpy = jest + .spyOn(Task, 'findOneAndUpdate') + .mockResolvedValueOnce(true); + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockedWBS); + const projectFindByIdSpy = jest.spyOn(Project, 'findById').mockResolvedValue(mockedProject); + + const response = await updateTaskStatus(mockReq, mockRes); + await flushPromises(); + + // assertResMock(201, null, response, mockRes); + expect(mockRes.status).toBeCalledWith(201); + expect(response).toBeUndefined(); + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindOneAndUpdateSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(projectFindByIdSpy).toHaveBeenCalled(); + }); + + test('Returns 400 on error', async () => { + const { updateTaskStatus } = makeSut(); + const error = new Error('some error'); + + mockReq.params = { + ...mockReq.params, + taskId: 456, + }; + + const taskFindByIdSpy = jest.spyOn(Task, 'findById').mockResolvedValue(mockedTask); + const taskFindOneAndUpdateSpy = jest + .spyOn(Task, 'findOneAndUpdate') + .mockRejectedValueOnce(error); + const wbsFindByIdSpy = jest.spyOn(WBS, 'findById').mockResolvedValue(mockedWBS); + const projectFindByIdSpy = jest.spyOn(Project, 'findById').mockResolvedValue(mockedProject); + + const response = await updateTaskStatus(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, error, response, mockRes); + + expect(taskFindByIdSpy).toHaveBeenCalled(); + expect(taskFindOneAndUpdateSpy).toHaveBeenCalled(); + expect(wbsFindByIdSpy).toHaveBeenCalled(); + expect(projectFindByIdSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 41f515e99..42d9d8d25 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -6,8 +6,66 @@ const Logger = require('../startup/logger'); const teamcontroller = function (Team) { const getAllTeams = function (req, res) { - Team.find({}) - .sort({ teamName: 1 }) + Team.aggregate([ + { + $unwind: '$members', + }, + { + $lookup: { + from: 'userProfiles', + localField: 'members.userId', + foreignField: '_id', + as: 'userProfile', + }, + }, + { + $unwind: '$userProfile', + }, + { + $match: { + isActive: true, + } + }, + { + $group: { + _id: { + teamId: '$_id', + teamCode: '$userProfile.teamCode', + }, + count: { $sum: 1 }, + teamName: { $first: '$teamName' }, + members: { + $push: { + _id: '$userProfile._id', + name: '$userProfile.name', + email: '$userProfile.email', + teamCode: '$userProfile.teamCode', + addDateTime: '$members.addDateTime', + }, + }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { count: -1 }, // Sort by the most frequent teamCode + }, + { + $group: { + _id: '$_id.teamId', + teamCode: { $first: '$_id.teamCode' }, // Get the most frequent teamCode + teamName: { $first: '$teamName' }, + members: { $first: '$members' }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { teamName: 1 }, // Sort teams by name + }, + ]) .then((results) => res.status(200).send(results)) .catch((error) => { Logger.logException(error); @@ -110,14 +168,15 @@ const teamcontroller = function (Team) { return; } - const canEditTeamCode = - req.body.requestor.role === 'Owner' || - req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); + // Removed the permission check as the permission check if done in earlier + // const canEditTeamCode = + // req.body.requestor.role === 'Owner' || + // req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); - if (!canEditTeamCode) { - res.status(403).send('You are not authorized to edit team code.'); - return; - } + // if (!canEditTeamCode) { + // res.status(403).send('You are not authorized to edit team code.'); + // return; + // } record.teamName = req.body.teamName; record.isActive = req.body.isActive; @@ -222,59 +281,62 @@ const teamcontroller = function (Team) { }, }, ]) - .then((result) => res.status(200).send(result)) + .then((result) => { + res.status(200).send(result) + }) .catch((error) => { Logger.logException(error, null, `TeamId: ${teamId} Request:${req.body}`); - res.status(500).send(error); + return res.status(500).send(error); }); }; const updateTeamVisibility = async (req, res) => { - console.log("==============> 9 "); + 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); + + 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() + + team + .save() + .then((updatedTeam) => { + // Additional operations after team.save() const assignlist = []; const unassignlist = []; - team.members.forEach(member => { + 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"); + 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' }); @@ -283,18 +345,17 @@ const teamcontroller = function (Team) { res.status(500).send({ error }); }); }) - .catch(errors => { + .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}`); + res.status(500).send(`Error updating team visibility: ${error.message}`); } }; - /** + /** * Leaner version of the teamcontroller.getAllTeams * Remove redundant data: members, isActive, createdDatetime, modifiedDatetime. */ @@ -308,7 +369,48 @@ const teamcontroller = function (Team) { res.status(500).send('Fetch team code failed.'); }); }; - + + const getAllTeamMembers = async function (req,res) { + try{ + const teamIds = req.body; + const cacheKey='teamMembersCache' + if(cache.hasCache(cacheKey)){ + let data=cache.getCache('teamMembersCache') + return res.status(200).send(data); + } + if (!Array.isArray(teamIds) || teamIds.length === 0 || !teamIds.every(team => mongoose.Types.ObjectId.isValid(team._id))) { + return res.status(400).send({ error: 'Invalid request: teamIds must be a non-empty array of valid ObjectId strings.' }); + } + let data = await Team.aggregate([ + { + $match: { _id: { $in: teamIds.map(team => mongoose.Types.ObjectId(team._id)) } } + }, + { $unwind: '$members' }, + { + $lookup: { + from: 'userProfiles', + localField: 'members.userId', + foreignField: '_id', + as: 'userProfile', + }, + }, + { $unwind: { path: '$userProfile', preserveNullAndEmptyArrays: true } }, + { + $group: { + _id: '$_id', // Group by team ID + teamName: { $first: '$teamName' }, // Use $first to keep the team name + createdDatetime: { $first: '$createdDatetime' }, + members: { $push: '$members' }, // Rebuild the members array + }, + }, + ]) + cache.setCache(cacheKey,data) + res.status(200).send(data); + }catch(error){ + console.log(error) + res.status(500).send({'message':"Fetching team members failed"}); + } + } return { getAllTeams, getAllTeamCode, @@ -319,6 +421,7 @@ const teamcontroller = function (Team) { assignTeamToUsers, getTeamMembership, updateTeamVisibility, + getAllTeamMembers }; }; diff --git a/src/controllers/timeZoneAPIController.js b/src/controllers/timeZoneAPIController.js index 07c9c0b17..2023f312a 100644 --- a/src/controllers/timeZoneAPIController.js +++ b/src/controllers/timeZoneAPIController.js @@ -1,11 +1,11 @@ // eslint-disable-next-line import/no-extraneous-dependencies const fetch = require('node-fetch'); +const dotenv = require('dotenv'); + +dotenv.config(); const ProfileInitialSetupToken = require('../models/profileInitialSetupToken'); const { hasPermission } = require('../utilities/permissions'); -const premiumKey = process.env.TIMEZONE_PREMIUM_KEY; -const commonKey = process.env.TIMEZONE_COMMON_KEY; - const performTimeZoneRequest = async (req, res, apiKey) => { const { location } = req.params; @@ -17,7 +17,6 @@ const performTimeZoneRequest = async (req, res, apiKey) => { try { const geocodeAPIEndpoint = 'https://api.opencagedata.com/geocode/v1/json'; const url = `${geocodeAPIEndpoint}?key=${apiKey}&q=${location}&pretty=1&limit=1`; - const response = await fetch(url); const data = await response.json(); @@ -53,16 +52,17 @@ const performTimeZoneRequest = async (req, res, apiKey) => { const timeZoneAPIController = function () { const getTimeZone = async (req, res) => { + const premiumKey = process.env.TIMEZONE_PREMIUM_KEY; + const commonKey = process.env.TIMEZONE_COMMON_KEY; const { requestor } = req.body; - if (!requestor.role) { res.status(403).send('Unauthorized Request'); return; } - const userAPIKey = (await hasPermission(requestor, 'getTimeZoneAPIKey')) ? premiumKey : commonKey; + if (!userAPIKey) { res.status(401).send('API Key Missing'); return; @@ -72,6 +72,7 @@ const timeZoneAPIController = function () { }; const getTimeZoneProfileInitialSetup = async (req, res) => { + const commonKey = process.env.TIMEZONE_COMMON_KEY; const { token } = req.body; if (!token) { res.status(400).send('Missing token'); diff --git a/src/controllers/timeZoneAPIController.spec.js b/src/controllers/timeZoneAPIController.spec.js new file mode 100644 index 000000000..2f3607cc6 --- /dev/null +++ b/src/controllers/timeZoneAPIController.spec.js @@ -0,0 +1,316 @@ +jest.mock('../utilities/permissions', () => ({ + hasPermission: jest.fn(), // Mocking the hasPermission function +})); + +jest.mock('node-fetch'); +// eslint-disable-next-line import/no-extraneous-dependencies +const fetch = require('node-fetch'); + +const originalPremiumKey = process.env.TIMEZONE_PREMIUM_KEY; +process.env.TIMEZONE_PREMIUM_KEY = 'mockPremiumKey'; + +const originalCommonKey = process.env.TIMEZONE_COMMON_KEY; +delete process.env.TIMEZONE_COMMON_KEY; + +const successfulFetchRequestWithResults = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + status: { + code: 200, + message: 'Request Processed Successfully', + }, + results: [ + { + annotations: { + timezone: { + name: 'timeZone - Fiji', + }, + }, + geometry: { + lat: 1, + lng: 1, + }, + components: { + country: 'U.S.', + city: 'Paris', + }, + }, + ], + }), + }), +); + +const successfulFetchRequestWithNoResults = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + status: { + code: 200, + message: 'Request Processed Successfully', + }, + results: [], + }), + }), +); + +const unsuccessfulFetchRequestInternalServerError = jest.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + status: { + code: null, + message: 'Internal Server Error', + }, + results: [], + }), + }), +); + +const { hasPermission } = require('../utilities/permissions'); +const timeZoneAPIController = require('./timeZoneAPIController'); +const ProfileInitialSetupToken = require('../models/profileInitialSetupToken'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const flushPromises = () => new Promise(setImmediate); +const makeSut = () => { + const { getTimeZone, getTimeZoneProfileInitialSetup } = timeZoneAPIController(); + return { getTimeZone, getTimeZoneProfileInitialSetup }; +}; + +describe('timeZoneAPIController Unit Tests', () => { + afterAll(() => { + // Reseting TIMEZONE_PREMIUM_KEY and TIMEZONE_COMMON_KEY environment variables to their original values + if (originalPremiumKey) { + process.env.TIMEZONE_PREMIUM_KEY = originalPremiumKey; + } else { + delete process.env.TIMEZONE_PREMIUM_KEY; + } + + if (originalCommonKey) { + process.env.TIMEZONE_COMMON_KEY = originalCommonKey; + } else { + delete process.env.TIMEZONE_COMMON_KEY; + } + }); + + describe('getTimeZone() function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeEach(() => { + hasPermission.mockResolvedValue(true); + }); + test('Returns 403, as requestor.role is missing in request body', async () => { + const { getTimeZone } = makeSut(); + + // setting request.role to `Null` + mockReq.body.requestor.role = null; + + const response = await getTimeZone(mockReq, mockRes); + + assertResMock(403, 'Unauthorized Request', response, mockRes); + }); + + test('Returns 401, as API is missing', async () => { + delete process.env.TIMEZONE_COMMON_KEY; + + const { getTimeZone } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + + hasPermission.mockResolvedValue(false); + + const response = await getTimeZone(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toBeCalledTimes(1); + assertResMock(401, 'API Key Missing', response, mockRes); + }); + + test('Returns 400, when `location` is missing in req.params', async () => { + const { getTimeZone } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + + const response = await getTimeZone(mockReq, mockRes); + await flushPromises(); + + expect(hasPermission).toBeCalledTimes(1); + assertResMock(400, 'Missing location', response, mockRes); + }); + + test('Returns 500, when status.code !== 200 and status code is missing', async () => { + const { getTimeZone } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + fetch.mockImplementation(unsuccessfulFetchRequestInternalServerError); + + const response = await getTimeZone(mockReq, mockRes); + await flushPromises(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(hasPermission).toBeCalledTimes(1); + assertResMock(500, 'opencage error- Internal Server Error', response, mockRes); + }); + + test('Returns 404, when status.code == 200 and data.results is empty', async () => { + const { getTimeZone } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + fetch.mockImplementation(successfulFetchRequestWithNoResults); + + const response = await getTimeZone(mockReq, mockRes); + await flushPromises(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(hasPermission).toBeCalledTimes(1); + assertResMock(404, 'No results found', response, mockRes); + }); + + test('Returns 200, when status.code == 200 and data.results is not empty', async () => { + const { getTimeZone } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + fetch.mockImplementation(successfulFetchRequestWithResults); + const timezone = 'timeZone - Fiji'; // mocking the timezone data to be returned by `successfulFetchRequestWithResults` + const currentLocation = { + // mocking the currentLocation data to be returned by `successfulFetchRequestWithResults` + userProvided: mockReq.params.location, + coords: { + lat: 1, + lng: 1, + }, + country: 'U.S.', + city: 'Paris', + }; + + const response = await getTimeZone(mockReq, mockRes); + await flushPromises(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(hasPermission).toBeCalledTimes(1); + assertResMock(200, { timezone, currentLocation }, response, mockRes); + }); + }); + + describe('getTimeZoneProfileInitialSetup() function', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + beforeEach(() => { + hasPermission.mockResolvedValue(true); + }); + + test('Returns status code 400 if token is missing in request.body', async () => { + mockReq.body.token = null; + + const { getTimeZoneProfileInitialSetup } = makeSut(); + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, 'Missing token', response, mockRes); + }); + + test('Returns status code 403 if token is missing in request.body', async () => { + mockReq.body.token = 'random_token_value'; + + const { getTimeZoneProfileInitialSetup } = makeSut(); + const profileInitialSetupTokenFindOneSpy = jest + .spyOn(ProfileInitialSetupToken, 'findOne') + .mockReturnValue(null); + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + expect(profileInitialSetupTokenFindOneSpy).toBeCalledTimes(1); + assertResMock(403, 'Unauthorized Request', response, mockRes); + }); + + test('Returns 500, when status.code !== 200 and status code is missing', async () => { + const { getTimeZoneProfileInitialSetup } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + const profileInitialSetupTokenFindOneSpy = jest + .spyOn(ProfileInitialSetupToken, 'findOne') + .mockReturnValue('token'); + fetch.mockImplementation(unsuccessfulFetchRequestInternalServerError); + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + expect(profileInitialSetupTokenFindOneSpy).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledTimes(1); + assertResMock(500, 'opencage error- Internal Server Error', response, mockRes); + }); + + test('Returns 404, when status.code == 200 and data.results is empty', async () => { + const { getTimeZoneProfileInitialSetup } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + const profileInitialSetupTokenFindOneSpy = jest + .spyOn(ProfileInitialSetupToken, 'findOne') + .mockReturnValue('token'); + fetch.mockImplementation(successfulFetchRequestWithNoResults); + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + expect(profileInitialSetupTokenFindOneSpy).toBeCalledTimes(1); + expect(fetch).toHaveBeenCalledTimes(1); + assertResMock(404, 'No results found', response, mockRes); + }); + + test('Returns 200, when status.code == 200 and data.results is not empty', async () => { + const { getTimeZoneProfileInitialSetup } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = 'New Jersey'; + + const profileInitialSetupTokenFindOneSpy = jest + .spyOn(ProfileInitialSetupToken, 'findOne') + .mockReturnValue('token'); + fetch.mockImplementation(successfulFetchRequestWithResults); + + const timezone = 'timeZone - Fiji'; // mocking the timezone data to be returned by `successfulFetchRequestWithResults` + const currentLocation = { + // mocking the currentLocation data to be returned by `successfulFetchRequestWithResults` + userProvided: mockReq.params.location, + coords: { + lat: 1, + lng: 1, + }, + country: 'U.S.', + city: 'Paris', + }; + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + expect(profileInitialSetupTokenFindOneSpy).toBeCalledTimes(1); + expect(fetch).toHaveBeenCalledTimes(1); + assertResMock(200, { timezone, currentLocation }, response, mockRes); + }); + + test('Returns 400, when `location` is missing in req.params', async () => { + const { getTimeZoneProfileInitialSetup } = makeSut(); + mockReq.body.requestor.role = 'Volunteer'; + mockReq.params.location = null; + + const profileInitialSetupTokenFindOneSpy = jest + .spyOn(ProfileInitialSetupToken, 'findOne') + .mockReturnValue('token'); + + const response = await getTimeZoneProfileInitialSetup(mockReq, mockRes); + await flushPromises(); + + expect(profileInitialSetupTokenFindOneSpy).toBeCalledTimes(1); + assertResMock(400, 'Missing location', response, mockRes); + }); + }); +}); diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index f0f69e146..e7a8662a6 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -19,6 +19,16 @@ const userProfileJobs = () => { } await userhelper.awardNewBadges(); await userhelper.reActivateUser(); + }, + null, + false, + 'America/Los_Angeles', + ); + + // Job to run every day, 1 minute past midnight to deactivate the user + const dailyUserDeactivateJobs = new CronJob( + '1 0 * * *', // Every day, 1 minute past midnight + async () => { await userhelper.deActivateUser(); }, null, @@ -27,5 +37,6 @@ const userProfileJobs = () => { ); allUserProfileJobs.start(); + dailyUserDeactivateJobs.start(); }; module.exports = userProfileJobs; diff --git a/src/models/jobs.js b/src/models/jobs.js new file mode 100644 index 000000000..b1f98a34a --- /dev/null +++ b/src/models/jobs.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const jobSchema = new Schema({ + title: { type: String, required: true }, // Job title + category: { type: String, required: true }, // General category (e.g., Engineering, Marketing) + description: { type: String, required: true }, // Detailed job description + imageUrl: { type: String, required: true }, // URL of the job-related image + location: { type: String, required: true }, // Job location (optional for remote jobs) + applyLink: { type: String, required: true }, // URL for the application form + featured: { type: Boolean, default: false }, // Whether the job should be featured prominently + datePosted: { type: Date, default: Date.now }, // Date the job was posted + jobDetailsLink: { type: String, required: true }, // Specific job details URL +}); + +module.exports = mongoose.model('Job', jobSchema); diff --git a/src/routes/informationRouter.test.js b/src/routes/informationRouter.test.js new file mode 100644 index 000000000..12a600723 --- /dev/null +++ b/src/routes/informationRouter.test.js @@ -0,0 +1,145 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const cache = require('../utilities/nodeCache')(); +const { app } = require('../app'); +const { + mockReq, + createUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); + +const agent = request.agent(app); + +describe('information routes', () => { + let user; + let token; + let reqBody = { + ...mockReq.body, + }; + beforeAll(async () => { + await dbConnect(); + user = await createUser(); + token = jwtPayload(user); + reqBody = { + ...reqBody, + infoName: 'some infoName', + infoContent: 'some infoContent', + visibility: '1', + }; + }); + beforeEach(async () => { + await dbClearCollections('informations'); + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + describe('informationRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/informations').send(reqBody).expect(401); + await agent.get('/api/informations/randomID').send(reqBody).expect(401); + }); + }); + describe('Post Information route', () => { + it('Should return 201 if the information is successfully added', async () => { + const response = await agent + .post('/api/informations') + .send(reqBody) + .set('Authorization', token) + .expect(201); + + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + infoName: reqBody.infoName, + infoContent: reqBody.infoContent, + visibility: reqBody.visibility, + }); + }); + }); + describe('Get Information route', () => { + it('Should return 201 if the information is successfully added', async () => { + const informations = [ + { + _id: '6605f860f948db61dab6f27m', + infoName: 'get info', + infoContent: 'get infoConten', + visibility: '1', + }, + ]; + cache.setCache('informations', JSON.stringify(informations)); + const response = await agent + .get('/api/informations') + .send(reqBody) + .set('Authorization', token) + .expect(200); + expect(response.body).toEqual({}); + }); + }); + describe('Delete Information route', () => { + it('Should return 400 if the route does not exist', async () => { + await agent + .delete('/api/informations/random123') + .send(reqBody) + .set('Authorization', token) + .expect(400); + }); + // thrown: "Exceeded timeout of 5000 ms for a test. + // Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." + // it('Should return 200 if deleting successfully', async () => { + // const _info = new Information(); + // _info.infoName = reqBody.infoName; + // _info.infoContent = reqBody.infoContent; + // _info.visibility = reqBody.visibility; + // const info = await _info.save(); + // const response = await agent + // .delete(`/api/informations/${info._id}`) + // .set('Authorization', token) + // .send(reqBody) + // .expect(200); + + // expect(response.body).toEqual( + // { + // _id: expect.anything(), + // __v: expect.anything(), + // infoName: info.infoName, + // infoContent: info.infoContent, + // visibility: info.visibility, + // }); + // }); + }); + describe('Update Information route', () => { + it('Should return 400 if the route does not exist', async () => { + await agent + .put('/api/informations/random123') + .send(reqBody) + .set('Authorization', token) + .expect(400); + }); + // thrown: "Exceeded timeout of 5000 ms for a test. + // Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout." + // it('Should return 200 if udapted successfully', async () => { + // const _info = new Information(); + // _info.infoName = reqBody.infoName; + // _info.infoContent = reqBody.infoContent; + // _info.visibility = reqBody.visibility; + // const info = await _info.save(); + + // const response = await agent + // .put(`/api/informations/${info.id}`) + // .send(reqBody) + // .set('Authorization', token) + // .expect(200); + // expect(response.body).toEqual( + // { + // _id: expect.anything(), + // __v: expect.anything(), + // infoName: info.infoName, + // infoContent: info.infoContent, + // visibility: info.visibility, + // }); + + // }); + }); +}); diff --git a/src/routes/jobsRouter.js b/src/routes/jobsRouter.js new file mode 100644 index 000000000..5b66735a6 --- /dev/null +++ b/src/routes/jobsRouter.js @@ -0,0 +1,14 @@ +const express = require('express'); +const jobsController = require('../controllers/jobsController'); // Adjust the path if needed + +const router = express.Router(); + +// Define routes +router.get('/', jobsController.getJobs); +router.get('/categories', jobsController.getCategories); +router.get('/:id', jobsController.getJobById); +router.post('/', jobsController.createJob); +router.put('/:id', jobsController.updateJob); +router.delete('/:id', jobsController.deleteJob); + +module.exports = router; diff --git a/src/routes/reasonRouter.test.js b/src/routes/reasonRouter.test.js new file mode 100644 index 000000000..a1f1ab6dc --- /dev/null +++ b/src/routes/reasonRouter.test.js @@ -0,0 +1,338 @@ +const request = require('supertest'); +const moment = require('moment-timezone'); +const { jwtPayload } = require('../test'); +const cache = require('../utilities/nodeCache')(); +const { app } = require('../app'); +const { + mockReq, + mockUser, + createUser, + createTestPermissions, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +// const Reason = require('../models/reason'); + +function mockDay(dayIdx, past = false) { + const date = moment().tz('America/Los_Angeles').startOf('day'); + while (date.day() !== dayIdx) { + date.add(past ? -1 : 1, 'days'); + } + return date; +} +const agent = request.agent(app); +describe('reason routers', () => { + let adminUser; + let adminToken; + let reqBody = { + body: { + ...mockReq.body, + ...mockUser(), + }, + }; + beforeAll(async () => { + await dbConnect(); + await createTestPermissions(); + adminUser = await createUser(); + adminToken = jwtPayload(adminUser); + }); + beforeEach(async () => { + await dbClearCollections('reason'); + await dbClearCollections('userProfiles'); + cache.setCache('allusers', '[]'); + reqBody = { + body: { + ...mockReq.body, + ...mockUser(), + reasonData: { + date: mockDay(0), + message: 'some reason', + }, + currentDate: moment.tz('America/Los_Angeles').startOf('day'), + }, + }; + }); + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + describe('reasonRouters', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/reason/').send(reqBody.body).expect(401); + await agent.get('/api/reason/randomId').send(reqBody.body).expect(401); + await agent.get('/api/reason/single/randomId').send(reqBody.body).expect(401); + await agent.patch('/api/reason/randomId/').send(reqBody.body).expect(401); + await agent.delete('/api/reason/randomId').send(reqBody.body).expect(401); + }); + }); + describe('Post reason route', () => { + it('Should return 400 if user did not choose SUNDAY', async () => { + reqBody.body.reasonData.date = mockDay(1, true); + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: + "You must choose the Sunday YOU'LL RETURN as your date. This is so your reason ends up as a note on that blue square.", + errorCode: 0, + }); + }); + it('Should return 400 if warning to choose a future date', async () => { + reqBody.body.reasonData.date = mockDay(0, true); + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: 'You should select a date that is yet to come', + errorCode: 7, + }); + }); + it('Should return 400 if not providing reason', async () => { + reqBody.body.reasonData.message = null; + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + message: 'You must provide a reason.', + errorCode: 6, + }); + }); + it('Should return 404 if error in finding user Id', async () => { + reqBody.body.userId = null; + const response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 403 if duplicate resonse', async () => { + // const userProfile = new userPro + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(403); + }); + it('Should return 200 if post successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post('/api/reason/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Get AllReason route', () => { + it('Should return 400 if route does not exist', async () => { + const response = await agent + .get(`/api/reason/random123`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(400); + expect(response.body).toEqual({ + errMessage: 'Something went wrong while fetching the user', + }); + }); + it('Should return 200 if get all reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .get(`/api/reason/${userId}`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Get Single Reason route', () => { + it('Should return 400 if route does not exist', async () => { + reqBody.query = { + queryDate: mockDay(1, true), + }; + const response = await agent + .get(`/api/reason/single/5a7e21f00317bc1538def4b9`) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 200 if get all reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + reqBody.query = { + queryDate: mockDay(1, true), + }; + response = await agent + .get(`/api/reason/single/${userId}`) + .set('Authorization', adminToken) + .expect(200); + }); + }); + describe('Patch reason route', () => { + it('Should return 404 if error in finding user Id', async () => { + reqBody.body.userId = null; + const response = await agent + .patch('/api/reason/5a7e21f00317bc1538def4b9/') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 404 if duplicate reasons', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .patch(`/api/reason/${userId}/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'Reason not found', + errorCode: 4, + }); + }); + it('Should return 200 if patch successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent.get('/api/userProfile').set('Authorization', adminToken).expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post(`/api/reason/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .patch(`/api/reason/${userId}/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toEqual({ + message: 'Reason Updated!', + }); + }); + }); + describe('Delete reason route', () => { + it('Should return 404 if route does not exist', async () => { + const response = await agent + .delete(`/api/reason/5a7e21f00317bc1538def4b9`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + expect(response.body).toEqual({ + message: 'User not found', + errorCode: 2, + }); + }); + it('Should return 200 if deleting successfully', async () => { + let response = await agent + .post('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toBeTruthy(); + response = await agent + .get('/api/userProfile') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + const userId = response.body[0]._id; + reqBody.body.userId = userId; + response = await agent + .post(`/api/reason/`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toBeTruthy(); + response = await agent + .delete(`/api/reason/${userId}`) + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(200); + expect(response.body).toEqual({ + message: 'Document deleted', + }); + }); + }); +}); diff --git a/src/routes/teamRouter.js b/src/routes/teamRouter.js index 1bf8cfc44..3fbd8abb5 100644 --- a/src/routes/teamRouter.js +++ b/src/routes/teamRouter.js @@ -11,6 +11,10 @@ const router = function (team) { .post(controller.postTeam) .put(controller.updateTeamVisibility); + teamRouter + .route("/team/reports") + .post(controller.getAllTeamMembers); + teamRouter .route('/team/:teamId') .get(controller.getTeamById) diff --git a/src/routes/timeZoneAPIRoutes.test.js b/src/routes/timeZoneAPIRoutes.test.js new file mode 100644 index 000000000..975e56ac2 --- /dev/null +++ b/src/routes/timeZoneAPIRoutes.test.js @@ -0,0 +1,208 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); + +const originalPremiumKey = process.env.TIMEZONE_PREMIUM_KEY; + +const { app } = require('../app'); +const { + mockReq, + mongoHelper: { dbConnect, dbDisconnect }, + createTestPermissions, + createUser, + mockUser, +} = require('../test'); + +const UserProfile = require('../models/userProfile'); +const ProfileInitialSetupToken = require('../models/profileInitialSetupToken'); + +const agent = request.agent(app); + +describe('timeZoneAPI routes', () => { + let adminUser; + let adminToken; + let volunteerUser; + let volunteerToken; + + const reqBody = {}; + const incorrectLocationParams = 'r'; + const locationParamsThatResultsInNoMatch = 'someReallyRandomLocation'; + const correctLocationParams = 'Berlin,+Germany'; + + beforeAll(async () => { + await dbConnect(); + await createTestPermissions(); + + reqBody.body = { + // This is the user we want to create + ...mockReq.body, + }; + adminUser = await createUser(); // This is the admin requestor user + adminToken = jwtPayload(adminUser); + + volunteerUser = mockUser(); // This is the admin requestor user + volunteerUser.email = 'volunteer@onecommunity.com'; + volunteerUser.role = 'Volunteer'; + volunteerUser = new UserProfile(volunteerUser); + volunteerUser = await volunteerUser.save(); + volunteerToken = jwtPayload(volunteerUser); + }); + + afterAll(async () => { + await dbDisconnect(); + + if (originalPremiumKey) { + process.env.TIMEZONE_PREMIUM_KEY = originalPremiumKey; + } else { + delete process.env.TIMEZONE_PREMIUM_KEY; + } + }); + + describe('API routes', () => { + it("should return 404 if route doesn't exist", async () => { + await agent + .post('/api/timezonesss') + .send(reqBody.body) + .set('Authorization', adminToken) + .expect(404); + }); + }); + + describe('getTimeZone - request parameter `location` based tests', () => { + test('401 when `API key` is missing', async () => { + const location = 'Berlin,+Germany'; + delete process.env.TIMEZONE_PREMIUM_KEY; + + const response = await agent + .get(`/api/timezone/${location}`) + .set('Authorization', adminToken) + .send(reqBody.body) + .expect(401); + + expect(response.error.text).toBe('API Key Missing'); + }); + + test('400 when `location` is incorrect', async () => { + const response = await agent + .get(`/api/timezone/${incorrectLocationParams}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(400); + + expect(response.error.text).toBeTruthy(); + }); + + test('200 when `location` is correctly formatted', async () => { + const response = await agent + .get(`/api/timezone/${correctLocationParams}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(200); + + expect(response).toBeTruthy(); + expect(response._body.timezone).toBeTruthy(); + expect(response._body.currentLocation).toBeTruthy(); + expect(response._body.currentLocation.userProvided).toBe(correctLocationParams); + }); + + test('404 when results.length === 0', async () => { + const response = await agent + .get(`/api/timezone/${locationParamsThatResultsInNoMatch}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(404); + + expect(response).toBeTruthy(); + }); + }); + + describe('getTimeZoneProfileInitialSetup - token is missing in body or in ProfileInitialSetupToken', () => { + test('401 when `token` is missing in request body', async () => { + const location = 'Berlin,+Germany'; + + const response = await agent + .post(`/api/timezone/${location}`) + .set('Authorization', adminToken) + .send(reqBody.body) + .expect(400); + + expect(response.error.text).toBe('Missing token'); + }); + + test('403 when ProfileInitialSetupToken does not contains `req.body.token`', async () => { + const location = 'Berlin,+Germany'; + reqBody.body = { + ...reqBody, + token: 'randomToken', + }; + + const response = await agent + .post(`/api/timezone/${location}`) + .set('Authorization', adminToken) + .send(reqBody.body) + .expect(403); + + expect(response.error.text).toBe('Unauthorized Request'); + }); + }); + + describe('getTimeZoneProfileInitialSetup - token is present in ProfileInitialSetupToken', () => { + const tokenData = 'randomToken'; + + beforeAll(async () => { + const expirationDate = new Date().setDate(new Date().getDate() + 10); + + let data = { + token: tokenData, + email: 'randomEmail', + weeklyCommittedHours: 5, + expiration: expirationDate, + createdDate: new Date(), + isCancelled: false, + isSetupCompleted: true, + }; + + data = new ProfileInitialSetupToken(data); + + // eslint-disable-next-line no-unused-vars + data = await data.save(); + + reqBody.body = { + ...reqBody, + token: tokenData, + }; + }); + + test('400 when `location` is incorrect', async () => { + const response = await agent + .get(`/api/timezone/${incorrectLocationParams}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(400); + + expect(response.error.text).toBeTruthy(); + }); + + test('200 when `location` is correctly formatted', async () => { + const response = await agent + .get(`/api/timezone/${correctLocationParams}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(200); + + expect(response).toBeTruthy(); + expect(response._body.timezone).toBeTruthy(); + expect(response._body.currentLocation).toBeTruthy(); + expect(response._body.currentLocation.userProvided).toBe(correctLocationParams); + }); + + test('404 when results.length === 0', async () => { + const response = await agent + .get(`/api/timezone/${locationParamsThatResultsInNoMatch}`) // Make sure this is the intended test + .set('Authorization', volunteerToken) + .send(reqBody.body) + .expect(404); + + expect(response).toBeTruthy(); + }); + }); +}); diff --git a/src/startup/db.js b/src/startup/db.js index c3c61807c..719c17f94 100644 --- a/src/startup/db.js +++ b/src/startup/db.js @@ -33,7 +33,7 @@ const afterConnect = async () => { module.exports = function () { const uri = `mongodb://${process.env.user}:${encodeURIComponent(process.env.password)}@${process.env.cluster}/${process.env.dbName}?ssl=true&replicaSet=${process.env.replicaSetName}&authSource=admin`; - + mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true, diff --git a/src/startup/middleware.js b/src/startup/middleware.js index eef3bd71b..400f5af6c 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -10,9 +10,8 @@ module.exports = function (app) { } if ( - (req.originalUrl === '/api/login' - || req.originalUrl === '/api/forgotpassword') - && req.method === 'POST' + (req.originalUrl === '/api/login' || req.originalUrl === '/api/forgotpassword') && + req.method === 'POST' ) { next(); return; @@ -21,12 +20,26 @@ module.exports = function (app) { next(); return; } - if (((req.originalUrl === '/api/ProfileInitialSetup' || req.originalUrl === '/api/validateToken' || req.originalUrl === '/api/getTimeZoneAPIKeyByToken') && req.method === 'POST') || (req.originalUrl === '/api/getTotalCountryCount' && req.method === 'GET') || (req.originalUrl.includes('/api/timezone') && req.method === 'POST') + if ( + ((req.originalUrl === '/api/ProfileInitialSetup' || + req.originalUrl === '/api/validateToken' || + req.originalUrl === '/api/getTimeZoneAPIKeyByToken') && + req.method === 'POST') || + (req.originalUrl === '/api/getTotalCountryCount' && req.method === 'GET') || + (req.originalUrl.includes('/api/timezone') && req.method === 'POST') ) { next(); return; } - if (req.originalUrl === '/api/add-non-hgn-email-subscription' || req.originalUrl === '/api/confirm-non-hgn-email-subscription' || req.originalUrl === '/api/remove-non-hgn-email-subscription' && req.method === 'POST') { + if ( + req.originalUrl === '/api/add-non-hgn-email-subscription' || + req.originalUrl === '/api/confirm-non-hgn-email-subscription' || + (req.originalUrl === '/api/remove-non-hgn-email-subscription' && req.method === 'POST') + ) { + next(); + return; + } + if (req.originalUrl.startsWith('/api/jobs') && req.method === 'GET') { next(); return; } @@ -44,13 +57,12 @@ module.exports = function (app) { res.status(401).send('Invalid token'); return; } - if ( - !payload - || !payload.expiryTimestamp - || !payload.userid - || !payload.role - || moment().isAfter(payload.expiryTimestamp) + !payload || + !payload.expiryTimestamp || + !payload.userid || + !payload.role || + moment().isAfter(payload.expiryTimestamp) ) { res.status(401).send('Unauthorized request'); return; diff --git a/src/startup/routes.js b/src/startup/routes.js index 82a4155a8..b307ac4f4 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -4,7 +4,6 @@ const project = require('../models/project'); const information = require('../models/information'); const team = require('../models/team'); // const actionItem = require('../models/actionItem'); -const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); const popup = require('../models/popupEditor'); @@ -55,6 +54,7 @@ const timeEntryRouter = require('../routes/timeentryRouter')(timeEntry); const projectRouter = require('../routes/projectRouter')(project); const informationRouter = require('../routes/informationRouter')(information); const teamRouter = require('../routes/teamRouter')(team); +const jobsRouter = require('../routes/jobsRouter'); // const actionItemRouter = require('../routes/actionItemRouter')(actionItem); const notificationRouter = require('../routes/notificationRouter')(); const loginRouter = require('../routes/loginRouter')(); @@ -162,6 +162,7 @@ module.exports = function (app) { app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); app.use('/api', blueSquareEmailAssignmentRouter); + app.use('/api/jobs', jobsRouter) // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); diff --git a/src/test/createTestPermissions.js b/src/test/createTestPermissions.js index 58623ea3f..e0f9eddf1 100644 --- a/src/test/createTestPermissions.js +++ b/src/test/createTestPermissions.js @@ -51,7 +51,9 @@ const permissionsRoles = [ 'changeUserStatus', 'updatePassword', 'deleteUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', // WBS 'postWbs', 'deleteWbs', @@ -111,7 +113,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'updateTask', 'putTeam', @@ -139,7 +143,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -194,6 +200,8 @@ const permissionsRoles = [ 'editTimeEntryToggleTangible', 'deleteTimeEntry', 'postTimeEntry', + 'sendEmails', + 'sendEmailToAll', 'updatePassword', 'getUserProfiles', 'getProjectMembers', @@ -202,7 +210,9 @@ const permissionsRoles = [ 'putUserProfileImportantInfo', 'updateSummaryRequirements', 'deleteUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 43dfec2a0..062679cdd 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -52,7 +52,9 @@ const permissionsRoles = [ 'changeUserRehireableStatus', 'updatePassword', 'deleteUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', 'manageAdminLinks', 'manageTimeOffRequests', 'changeUserRehireableStatus', @@ -149,7 +151,6 @@ const permissionsRoles = [ { roleName: 'Mentor', permissions: [ - 'updateTask', 'suggestTask', 'putReviewStatus', 'getReporteesLimitRoles', @@ -212,6 +213,8 @@ const permissionsRoles = [ 'editTimeEntryToggleTangible', 'deleteTimeEntry', 'postTimeEntry', + 'sendEmails', + 'sendEmailToAll', 'updatePassword', 'getUserProfiles', 'getProjectMembers', @@ -220,7 +223,9 @@ const permissionsRoles = [ 'putUserProfileImportantInfo', 'updateSummaryRequirements', 'deleteUserProfile', - 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', + 'deleteInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', @@ -251,7 +256,10 @@ const permissionsRoles = [ 'seeUsersInDashboard', 'changeUserRehireableStatus', - 'manageAdminLinks', + + 'removeUserFromTask', + + 'editHeaderMessage', ], }, ]; diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 19834abf4..839004e52 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -2,57 +2,72 @@ const nodemailer = require('nodemailer'); const { google } = require('googleapis'); const logger = require('../startup/logger'); -const closure = () => { - const queue = []; +const config = { + email: process.env.REACT_APP_EMAIL, + clientId: process.env.REACT_APP_EMAIL_CLIENT_ID, + clientSecret: process.env.REACT_APP_EMAIL_CLIENT_SECRET, + redirectUri: process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI, + refreshToken: process.env.REACT_APP_EMAIL_REFRESH_TOKEN, + batchSize: 50, + concurrency: 3, + rateLimitDelay: 1000, +}; - const CLIENT_EMAIL = process.env.REACT_APP_EMAIL; - const CLIENT_ID = process.env.REACT_APP_EMAIL_CLIENT_ID; - const CLIENT_SECRET = process.env.REACT_APP_EMAIL_CLIENT_SECRET; - const REDIRECT_URI = process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI; - const REFRESH_TOKEN = process.env.REACT_APP_EMAIL_REFRESH_TOKEN; - // Create the email envelope (transport) - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - type: 'OAuth2', - user: CLIENT_EMAIL, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }, - }); +const OAuth2Client = new google.auth.OAuth2( + config.clientId, + config.clientSecret, + config.redirectUri, +); +OAuth2Client.setCredentials({ refresh_token: config.refreshToken }); - const OAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); +// Create the email envelope (transport) +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + type: 'OAuth2', + user: config.email, + clientId: config.clientId, + clientSecret: config.clientSecret, + }, +}); - OAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); +const sendEmail = async (mailOptions) => { + try { + const { token } = await OAuth2Client.getAccessToken(); + mailOptions.auth = { + user: config.email, + refreshToken: config.refreshToken, + accessToken: token, + }; + const result = await transporter.sendMail(mailOptions); + if (process.env.NODE_ENV === 'local') { + logger.logInfo(`Email sent: ${JSON.stringify(result)}`); + } + return result; + } catch (error) { + logger.logException(error, `Error sending email: ${mailOptions.to}`); + throw error; + } +}; - setInterval(async () => { - const nextItem = queue.shift(); +const queue = []; +let isProcessing = false; - if (!nextItem) return; +const { recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt, resolve, reject} = nextItem; - const { recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt, resolve, reject} = nextItem; +const processQueue = async () => { + if (isProcessing || queue.length === 0) re - try { - // Generate the accessToken on the fly - const res = await OAuth2Client.getAccessToken(); - const ACCESSTOKEN = res.token; - - const mailOptions = { - from: CLIENT_EMAIL, - to: recipient, - cc, - bcc, - subject, - html: message, - replyTo, - auth: { - user: CLIENT_EMAIL, - refreshToken: REFRESH_TOKEN, - accessToken: ACCESSTOKEN, - }, - }; - - const result = await transporter.sendMail(mailOptions); + isProcessing = true; + console.log('Processing email queue...'); + + const processBatch = async () => { + if (queue.length === 0) { + isProcessing = false; + return; + } + + const result = await transporter.sendMail(mailOptions); if (typeof acknowledgingReceipt === 'function') { acknowledgingReceipt(null, result); } @@ -76,8 +91,14 @@ const closure = () => { `Extra Data: cc ${cc} bcc ${bcc}`, ); reject(error); + const batch = queue.shift(); + try { + console.log('Sending email...'); + await sendEmail(batch); + } catch (error) { + logger.logException(error, 'Failed to send email batch'); } - }, process.env.MAIL_QUEUE_INTERVAL || 1000); + const emailSender = function ( recipient, @@ -105,9 +126,43 @@ const closure = () => { resolve('Email sending is disabled'); } }); + setTimeout(processBatch, config.rateLimitDelay); + }; - return emailSender; + const concurrentProcesses = Array(config.concurrency).fill().map(processBatch); + + try { + await Promise.all(concurrentProcesses); + } finally { + isProcessing = false; + } +}; + +const emailSender = ( + recipients, + subject, + message, + attachments = null, + cc = null, + replyTo = null, +) => { + if (!process.env.sendEmail) return; + const recipientsArray = Array.isArray(recipients) ? recipients : [recipients]; + for (let i = 0; i < recipients.length; i += config.batchSize) { + const batchRecipients = recipientsArray.slice(i, i + config.batchSize); + queue.push({ + from: config.email, + bcc: batchRecipients.join(','), + subject, + html: message, + attachments, + cc, + replyTo, + }); + } + console.log('Emails queued:', queue.length); + setImmediate(processQueue); }; -module.exports = closure(); +module.exports = emailSender; From 64f3ff90327f8aa09236a65cd4e1253adc9705cf Mon Sep 17 00:00:00 2001 From: Ankuriboh <183397864+Ankuriboh@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:34:32 -0500 Subject: [PATCH 45/48] fix: revert emailSender.js to e7d5baa19ad9c3c41aeb7a9bdf869fdb6c0a52dd. --- src/utilities/emailSender.js | 56 +----------------------------------- 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 839004e52..b0fb40112 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -53,10 +53,8 @@ const sendEmail = async (mailOptions) => { const queue = []; let isProcessing = false; -const { recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt, resolve, reject} = nextItem; - const processQueue = async () => { - if (isProcessing || queue.length === 0) re + if (isProcessing || queue.length === 0) return; isProcessing = true; console.log('Processing email queue...'); @@ -67,30 +65,6 @@ const processQueue = async () => { return; } - const result = await transporter.sendMail(mailOptions); - 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)}`); - } - resolve(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}`, - ); - reject(error); const batch = queue.shift(); try { console.log('Sending email...'); @@ -99,35 +73,7 @@ const processQueue = async () => { logger.logException(error, 'Failed to send email batch'); } - - const emailSender = function ( - recipient, - subject, - message, - cc = null, - bcc = null, - replyTo = null, - acknowledgingReceipt = null, - ) { - return new Promise((resolve, reject) => { - if (process.env.sendEmail) { - queue.push({ - recipient, - subject, - message, - cc, - bcc, - replyTo, - acknowledgingReceipt, - resolve, - reject, - }); - } else { - resolve('Email sending is disabled'); - } - }); setTimeout(processBatch, config.rateLimitDelay); - }; const concurrentProcesses = Array(config.concurrency).fill().map(processBatch); From f4e8827a067a9bb9dff904001a7ed24fa8879c7e Mon Sep 17 00:00:00 2001 From: SFA23SCM35V Date: Mon, 16 Dec 2024 15:41:56 -0600 Subject: [PATCH 46/48] commiting changes --- package-lock.json | 92 ++++++++--------- src/controllers/userProfileController.js | 122 ++++++++--------------- 2 files changed, 85 insertions(+), 129 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c4e3e99e..71f9e3e74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2857,7 +2857,7 @@ "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, "@types/methods": { @@ -3081,7 +3081,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "array-includes": { "version": "3.1.6", @@ -4739,7 +4739,7 @@ "bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, "bignumber.js": { "version": "9.0.2", @@ -4869,7 +4869,7 @@ "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-from": { "version": "1.1.2", @@ -5036,7 +5036,7 @@ "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" }, "clone-deep": { "version": "4.0.1", @@ -5100,7 +5100,7 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "component-emitter": { "version": "1.3.1", @@ -5110,7 +5110,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "confusing-browser-globals": { "version": "1.0.11", @@ -5154,7 +5154,7 @@ "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { "version": "2.1.4", @@ -5616,7 +5616,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "electron-to-chromium": { "version": "1.4.81", @@ -5638,7 +5638,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "end-of-stream": { "version": "1.4.4", @@ -5783,7 +5783,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -6718,7 +6718,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "event-target-shim": { "version": "5.0.1", @@ -6881,7 +6881,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "safe-buffer": { "version": "5.2.1", @@ -6926,7 +6926,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "fast-safe-stringify": { @@ -7000,7 +7000,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" } } }, @@ -7137,7 +7137,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fs-constants": { "version": "1.0.0", @@ -7153,7 +7153,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.3.3", @@ -7666,7 +7666,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "indent-string": { @@ -7678,7 +7678,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7872,7 +7872,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -7986,13 +7986,13 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "istanbul-lib-coverage": { "version": "3.2.2", @@ -10482,7 +10482,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, "json5": { @@ -11036,7 +11036,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memory-pager": { "version": "1.5.0", @@ -11047,7 +11047,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "merge-stream": { "version": "2.0.0", @@ -11058,7 +11058,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { "version": "4.0.5", @@ -11385,7 +11385,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, "negotiator": { @@ -11552,7 +11552,7 @@ "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==" + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { "version": "1.12.0", @@ -12271,7 +12271,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } @@ -12361,7 +12361,7 @@ "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==" + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" }, "parse-srcset": { "version": "1.0.2", @@ -12381,7 +12381,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -12397,7 +12397,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "pend": { "version": "1.2.0", @@ -13150,7 +13150,7 @@ "sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", "optional": true, "requires": { "memory-pager": "^1.0.2" @@ -13900,7 +13900,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, "strip-final-newline": { @@ -14091,13 +14091,13 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "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": { @@ -14115,7 +14115,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-regex-range": { "version": "5.0.1", @@ -14142,7 +14142,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "tsconfig-paths": { "version": "3.14.2", @@ -14308,7 +14308,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "update-browserslist-db": { "version": "1.0.16", @@ -14346,17 +14346,17 @@ "url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + "integrity": "sha1-/FZaPMy/93MMd19WQflVV5FDnyE=" }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { "version": "3.4.0", @@ -14420,7 +14420,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "walker": { "version": "1.0.8", @@ -14434,12 +14434,12 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -14557,7 +14557,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "4.0.2", diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 27bb4fd1b..6a23dd34b 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1493,26 +1493,7 @@ const userProfileController = function (UserProfile, Project) { res.status(200).send({ refreshToken: currentRefreshToken }); }; - // Search for user by first name - // const getUserBySingleName = (req, res) => { - // const pattern = new RegExp(`^${ req.params.singleName}`, 'i'); - - // // Searches for first or last name - // UserProfile.find({ - // $or: [ - // { firstName: { $regex: pattern } }, - // { lastName: { $regex: pattern } }, - // ], - // }) - // .select('firstName lastName') - // .then((users) => { - // 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)); - // }; + const getUserBySingleName = (req, res) => { const pattern = new RegExp(`^${req.params.singleName}`, 'i'); @@ -1557,13 +1538,7 @@ const userProfileController = function (UserProfile, Project) { .catch((error) => res.status(500).send(error)); }; - // function escapeRegExp(string) { - // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // } - /** - * Authorizes user to be able to add Weekly Report Recipients - * - */ + const authorizeUser = async (req, res) => { try { let authorizedUser; @@ -1707,66 +1682,47 @@ const userProfileController = function (UserProfile, Project) { }); }; + const deleteInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { + res.status(403).send('You are not authorized to delete blue square'); + return; + } + const { userId, blueSquareId } = req.params; + // console.log(userId, blueSquareId); - const deleteInfringements = async (req, res) => { - try { - // Check permissions - if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { - return res.status(403).send('You are not authorized to delete blue square'); - } - - const { userId, blueSquareId } = req.params; - const record = await UserProfile.findById(userId).exec(); - - // Check if the record exists - if (!record) { - return res.status(404).send('No valid records found'); + UserProfile.findById(userId, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; } - - const originalInfringements = record.infringements || []; - - // Filter out the infringement to be deleted - record.infringements = originalInfringements.filter( - (infringement) => !infringement._id.equals(blueSquareId) + + const originalinfringements = record?.infringements ?? []; + + record.infringements = originalinfringements.filter( + (infringement) => !infringement._id.equals(blueSquareId), ); -// <<<<<<< vorugantisaivenkatesh--Improve-speed-and-function-of-deleting-multiple-blue-squares - -// // Save the updated record -// const updatedRecord = await record.save(); - -// // Notify about the updated infringements -// userHelper.notifyInfringements(originalInfringements, updatedRecord.infringements); - -// // Respond with success -// res.status(200).json({ _id: updatedRecord._id }); -// } catch (error) { -// res.status(400).send(error.message); -// } -// ======= - -// record -// .save() -// .then((results) => { -// userHelper.notifyInfringements( -// originalinfringements, -// results.infringements, -// results.firstName, -// results.lastName, -// results.email, -// results.role, -// results.startDate, -// results.jobTitle[0], -// results.weeklycommittedHours, -// ); -// res.status(200).json({ -// _id: record._id, -// }); -// }) -// .catch((error) => res.status(400).send(error)); -// }); -// >>>>>>> development + + record + .save() + .then((results) => { + userHelper.notifyInfringements( + originalinfringements, + results.infringements, + results.firstName, + results.lastName, + results.email, + results.role, + results.startDate, + results.jobTitle[0], + results.weeklycommittedHours, + ); + res.status(200).json({ + _id: record._id, + }); + }) + .catch((error) => res.status(400).send(error)); + }); }; - const getProjectsByPerson = async function (req, res) { try { From 4de52b7088dcf077cf8dea749f5a5dff84131419 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Mon, 16 Dec 2024 20:06:17 -0800 Subject: [PATCH 47/48] incorporated changes requested --- src/controllers/currentWarningsController.js | 6 ++---- src/controllers/warningsController.js | 10 +--------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/controllers/currentWarningsController.js b/src/controllers/currentWarningsController.js index f064cb4f7..ec76d3326 100644 --- a/src/controllers/currentWarningsController.js +++ b/src/controllers/currentWarningsController.js @@ -34,7 +34,7 @@ const currentWarningsController = function (currentWarnings) { const warnings = await currentWarnings.find({}); if (warnings.length === 0) { - return res.status(400).send({ message: 'no valid records' }); + return res.status(400).send({ error: 'no valid records' }); } const testWarning = checkIfSpecialCharacter(newWarning); @@ -136,9 +136,7 @@ const currentWarningsController = function (currentWarnings) { }, ); - res.status(200).send({ - message: 'warning description was successfully deleted and user profiles updated', - }); + return res.status(200); } catch (error) { res.status(401).send({ message: error.message || error }); } diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index fb1aa3eef..e75c9f84c 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -27,21 +27,13 @@ async function getWarningDescriptions() { currentWarningDescriptions = await currentWarnings.find({}, { warningTitle: 1, _id: 0 }); } -const convertObjectToArray = (obj) => { - const arr = []; - for (const key of obj) { - arr.push(key.warningTitle); - } - return arr; -}; - const warningsController = function (UserProfile) { const getWarningsByUserId = async function (req, res) { currentWarningDescriptions = await currentWarnings.find({ activeWarning: true, }); - currentWarningDescriptions = convertObjectToArray(currentWarningDescriptions); + currentWarningDescriptions = currentWarningDescriptions.map((a) => a.warningTitle); const { userId } = req.params; try { From 339a1fe7a4ffc0e56b1008d66950e57649fa85e5 Mon Sep 17 00:00:00 2001 From: luisarevalo21 Date: Tue, 17 Dec 2024 20:05:15 -0800 Subject: [PATCH 48/48] adjusted the email sender --- src/controllers/warningsController.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index e75c9f84c..08515c207 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -185,6 +185,7 @@ const sendEmailToUser = ( `${userAssignedWarning.email}`, subjectTitle, emailTemplate, + null, adminEmails.toString(), null, ); @@ -192,6 +193,7 @@ const sendEmailToUser = ( emailSender( `${userAssignedWarning.email}`, `Blue Square issued for ${warningDescription}`, + null, emailTemplate, adminEmails.toString(), null,