diff --git a/constants/routes.constant.js b/constants/routes.constant.js index 4be1d490..119ee158 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -215,9 +215,13 @@ const searchRoutes = { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/search/" }, - "bactchAction": { + "updateStatus": { requestType: Constants.REQUEST_TYPES.GET, - uri: "/api/search/action", + uri: "/api/search/updateStatus", + }, + "sendEmails": { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/search/sendEmails", }, }; diff --git a/constants/success.constant.js b/constants/success.constant.js index acebf1d8..d7a8c1d9 100644 --- a/constants/success.constant.js +++ b/constants/success.constant.js @@ -21,6 +21,7 @@ const HACKER_GET_BY_ID = "Hacker found by id."; const HACKER_READ = "Hacker retrieval successful."; const HACKER_CREATE = "Hacker creation successful."; const HACKER_UPDATE = "Hacker update successful."; +const HACKER_UPDATE_EMAILS = "Hacker update emails sent." const HACKER_SENT_WEEK_OF = "Hacker week-of email sent." const RESUME_UPLOAD = "Resume upload successful."; @@ -67,6 +68,7 @@ module.exports = { HACKER_READ: HACKER_READ, HACKER_CREATE: HACKER_CREATE, HACKER_UPDATE: HACKER_UPDATE, + HACKER_UPDATE_EMAILS: HACKER_UPDATE_EMAILS, HACKER_SENT_WEEK_OF: HACKER_SENT_WEEK_OF, diff --git a/controllers/search.controller.js b/controllers/search.controller.js index 52ca25de..0322da7a 100644 --- a/controllers/search.controller.js +++ b/controllers/search.controller.js @@ -5,6 +5,7 @@ const Services = { }; const Util = require("../middlewares/util.middleware"); const Success = require("../constants/success.constant"); +const ErrorMessages = require("../constants/error.constant") async function searchResults(req, res) { let results = req.body.results; @@ -21,6 +22,22 @@ async function searchResults(req, res) { }); } +async function emailResults(req, res) { + let results = req.body.results; + let message; + if (results == undefined) { + message = Success.HACKER_UPDATE_EMAILS; + results = {} + } else { + message = ErrorMessages.EMAIL_500_MESSAGE; + } + return res.status(200).json({ + message: message, + data: results + }); +} + module.exports = { - searchResults: Util.asyncMiddleware(searchResults) + searchResults: Util.asyncMiddleware(searchResults), + emailResults: Util.asyncMiddleware(emailResults) }; \ No newline at end of file diff --git a/middlewares/search.middleware.js b/middlewares/search.middleware.js index f40ef915..94c39b96 100644 --- a/middlewares/search.middleware.js +++ b/middlewares/search.middleware.js @@ -5,7 +5,6 @@ const Services = { const Middleware = { Util: require("../middlewares/util.middleware") } -const Constants = require("../../constants/general.constant"); /** * @function parseQuery @@ -71,9 +70,28 @@ async function executeQuery(req, res, next) { * @returns */ async function executeStatusAction(req, res, next) { - // NOW HAVE req.body.results as an array of "hackers potentially" - console.log("GETS IN EXECUTE STATUS ACTION BRO!") - req.body.results = await Services.Search.executeAction(req.body.model, + req.body.results = await Services.Search.executeStatusAction(req.body.model, + req.body.q, + req.body.page, + req.body.limit, + req.body.sort, + req.body.sort_by, + req.body.expand, + req.body.update + ); + return next(); +} + +/** + * + * @param {} req + * @param {*} res + * @param {*} next + * + * @returns + */ +async function executeEmailAction(req, res, next) { + req.body.results = await Services.Search.executeEmailAction(req.body.model, req.body.q, req.body.page, req.body.limit, @@ -95,5 +113,6 @@ module.exports = { parseQuery: parseQuery, executeQuery: Middleware.Util.asyncMiddleware(executeQuery), executeStatusAction: Middleware.Util.asyncMiddleware(executeStatusAction), + executeEmailAction: Middleware.Util.asyncMiddleware(executeEmailAction), setExpandTrue: setExpandTrue, }; \ No newline at end of file diff --git a/middlewares/validators/search.validator.js b/middlewares/validators/search.validator.js index 0ca231a7..4e238b7b 100644 --- a/middlewares/validators/search.validator.js +++ b/middlewares/validators/search.validator.js @@ -11,7 +11,7 @@ module.exports = { VALIDATOR.booleanValidator("query", "expand", true), VALIDATOR.searchValidator("query", "q") ], - searchActionValidator: [ + statusValidator: [ VALIDATOR.searchModelValidator("query", "model", false), VALIDATOR.alphaValidator("query", "sort", true), VALIDATOR.integerValidator("query", "page", true, 0), @@ -19,8 +19,16 @@ module.exports = { VALIDATOR.searchSortValidator("query", "sort_by"), VALIDATOR.booleanValidator("query", "expand", true), VALIDATOR.searchValidator("query", "q"), - /* ACTION VALIDATOR NEED TO MAKE WORK! */ - VALIDATOR.actionValidator("query", "action"), - VALIDATOR.statusValidator("query", "status") + VALIDATOR.updateObjectValidator("query", "update") ], + emailValidator: [ + VALIDATOR.searchModelValidator("query", "model", false), + VALIDATOR.alphaValidator("query", "sort", true), + VALIDATOR.integerValidator("query", "page", true, 0), + VALIDATOR.integerValidator("query", "limit", true, 0, 1000), + VALIDATOR.searchSortValidator("query", "sort_by"), + VALIDATOR.booleanValidator("query", "expand", true), + VALIDATOR.searchValidator("query", "q"), + VALIDATOR.statusValidator("query", "status") + ] }; \ No newline at end of file diff --git a/middlewares/validators/validator.helper.js b/middlewares/validators/validator.helper.js index fe387737..db96f2b0 100644 --- a/middlewares/validators/validator.helper.js +++ b/middlewares/validators/validator.helper.js @@ -566,12 +566,43 @@ function actionValidator(fieldLocation, actionFieldName) { } +/** + * Validates that action field is a valid action from constants passed, and checks if corresponding new status is valid. + * @param {"query" | "body" | "header" | "param"} fieldLocation The location where the field should be found. + * @param {string} actionFieldName The name of the action that needs to be performed. + * @param {string} statusFieldName The name of the action that needs to be performed. + */ +function updateObjectValidator(fieldLocation, actionFieldName) { + const updateObjectValue = setProperValidationChainBuilder(fieldLocation, actionFieldName, "Invalid update object string."); + + return updateObjectValue.exists() + .withMessage("The update object string must exist.") + .custom(updateObjectValidatorHelper).withMessage("The value must be a valid update object."); +} + +function updateObjectValidatorHelper(update) { + try { + var updateObject = JSON.parse(update); + + if (updateObject && typeof updateObject === "object" && !("password" in updateObject)) { + for (var key in updateObject) { + var schemaPath = Models.Hacker.searchableField(key); + if (!schemaPath) return false; + } + return true; + } + } + catch (e) { + return false; + } +} + function statusValidator(fieldLocation, statusFieldName) { const statusValue = setProperValidationChainBuilder(fieldLocation, statusFieldName, "Invalid status."); return statusValue.exists().withMessage("The status must exist!").custom((val, { req }) => { - return Constants.CORRESPONDING_STATUSES[req.query.action].includes(val); + return Constants.HACKER_STATUSES.includes(val); }).withMessage("The value must be a proper status.") } @@ -647,5 +678,6 @@ module.exports = { enumValidator: enumValidator, routesValidator: routesValidator, actionValidator: actionValidator, - statusValidator: statusValidator + statusValidator: statusValidator, + updateObjectValidator: updateObjectValidator, }; \ No newline at end of file diff --git a/routes/api/search.js b/routes/api/search.js index 98416823..bdf680b8 100644 --- a/routes/api/search.js +++ b/routes/api/search.js @@ -68,7 +68,7 @@ module.exports = { ); /** - * @api {get} /search/action execute an action on a specific query for any defined model + * @api {get} /search/updateStatus execute an action on a specific query for any defined model * @apiName search * @apiGroup Search * @apiVersion 0.0.8 @@ -81,8 +81,7 @@ module.exports = { * @apiParam (query) {number} limit the maximum number of results that you would like returned * @apiParam (query) {any} sort_by any parameter you want to sort the results by * @apiParam (query) {boolean} expand whether you want to expand sub documents within the results - * @apiParam (query) {String} action type of action either Status or Email - * @apiParam (query) {String} status new status or type of email + * @apiParam (query) {String} update new status or type of email * * @apiSuccess {String} message Success message * @apiSuccess {Object} data Results @@ -107,16 +106,26 @@ module.exports = { * @apiErrorExample {object} Error-Response: * {"message": "Validation failed", "data": {}} */ - searchRouter.route("/action").get( + searchRouter.route("/updateStatus").get( Middleware.Auth.ensureAuthenticated(), Middleware.Auth.ensureAuthorized(), - Middleware.Validator.Search.searchActionValidator, + Middleware.Validator.Search.statusValidator, Middleware.parseBody.middleware, Middleware.Search.parseQuery, Middleware.Search.executeStatusAction, Controllers.Search.searchResults ); + searchRouter.route("/sendEmails").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), + Middleware.Validator.Search.emailValidator, + Middleware.parseBody.middleware, + Middleware.Search.parseQuery, + Middleware.Search.executeEmailAction, + Controllers.Search.emailResults + ); + apiRouter.use("/search", searchRouter); } }; \ No newline at end of file diff --git a/services/email.service.js b/services/email.service.js index ccde0da8..619ee0ad 100644 --- a/services/email.service.js +++ b/services/email.service.js @@ -18,7 +18,7 @@ class EmailService { * @param {*} mailData * @param {(err?)=>void} callback */ - send(mailData, callback = () => {}) { + send(mailData, callback = () => { }) { if (env.isTest()) { //Silence all actual emails if we're testing mailData.mailSettings = { @@ -41,7 +41,7 @@ class EmailService { * @param {*} mailData * @param {(err?)=>void} callback */ - sendMultiple(mailData, callback = () => {}) { + sendMultiple(mailData, callback = () => { }) { return client.sendMultiple(mailData, (error) => { if (error) { logger.error(`${TAG} ` + JSON.stringify(error)); @@ -100,6 +100,27 @@ class EmailService { } }, callback); } + + async sendStatusUpdateAsync(firstName, recipient, status) { + const handlebarsPath = path.join(__dirname, `../assets/email/statusEmail/${status}.hbs`); + const mailData = { + to: recipient, + from: process.env.NO_REPLY_EMAIL, + subject: Constants.EMAIL_SUBJECTS[status], + html: this.renderEmail(handlebarsPath, { + firstName: firstName + }) + }; + return this.send(mailData).then( + (response) => { + if (response[0].statusCode >= 200 && response[0].statusCode < 300) { + return undefined; + } else { + return response[0]; + } + }); + } + /** * Generates the HTML from the handlebars template file found at the given path. * @param {string} path the absolute path to the handlebars template file diff --git a/services/search.service.js b/services/search.service.js index 71a787b2..da69f30d 100644 --- a/services/search.service.js +++ b/services/search.service.js @@ -1,19 +1,20 @@ "use strict"; const Hacker = require("../models/hacker.model"); +const EmailService = require("./email.service") +const AccountService = require("./account.service") const logger = require("./logger.service"); /** - * @function executeQuery + * @function createQuery * @param {string} model the model which is being searched * @param {Array} queryArray array of clauses for the query * @param {number} page the page number you want * @param {number} limit the limit to the number of responses you want * @param {"asc"|"desc"} sort which direction you want to sort by - * @param {string} sort_by the attribute you want to sort by - * @returns {Promise<[Array]>} - * @description Builds and executes a search query based on a subset of mongodb + * @param {string} sortBy the attribute you want to sort by + * @returns {Query} Builds a query object and returns it based on the parameters provided */ -function executeQuery(model, queryArray, page, limit, sort, sort_by, shouldExpand = false) { +function createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand = false) { var query; switch (model.toLowerCase()) { case "hacker": @@ -63,104 +64,89 @@ function executeQuery(model, queryArray, page, limit, sort, sort_by, shouldExpan } if (sort == "desc") { - query.sort("-" + sort_by); + query.sort("-" + sortBy); } else if (sort == "asc") { - query.sort(sort_by); + query.sort(sortBy); } - return query.limit(limit) - .skip(limit * page) - .exec(); + return query.limit(limit).skip(limit * page) } /** - * @function executeAction + * @function executeQuery * @param {string} model the model which is being searched * @param {Array} queryArray array of clauses for the query * @param {number} page the page number you want * @param {number} limit the limit to the number of responses you want * @param {"asc"|"desc"} sort which direction you want to sort by - * @param {string} sort_by the attribute you want to sort by - * @param {string} new_status the status you want to set of the queried model + * @param {string} sortBy the attribute you want to sort by * @returns {Promise<[Array]>} * @description Builds and executes a search query based on a subset of mongodb */ -function executeAction(model, queryArray, page, limit, sort, sort_by, shouldExpand = false, new_status) { - var query; - console.log("GETS IN EXECUTE ACTION BRO!") - switch (model.toLowerCase()) { - case "hacker": - query = (shouldExpand) ? Hacker.find().populate([{ - path: "accountId", - select: " -password" - }, { - path: "teamId" - }]) : Hacker.find(); - break; - default: - return []; - } - for (var i in queryArray) { - var clause = queryArray[i]; - var param = clause.param; - var val = clause.value; - switch (clause.operation) { - case "equals": - query.where(param).equals(val); - break; - case "ne": - query.where(param).ne(val); - break; - case "lt": - query.where(param).lt(val); - break; - case "gt": - query.where(param).gt(val); - break; - case "lte": - query.where(param).lte(val); - break; - case "gte": - query.where(param).gte(val); - break; - case "in": - query.where(param).in(val); - break; - case "regex": - query.where(param).regex(val); - break; - case "elemMatch": - query.where(param).elemMatch(val); - break; - } - } +function executeQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand = false) { + var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); + return query.exec(); +} - if (sort == "desc") { - query.sort("-" + sort_by); - } else if (sort == "asc") { - query.sort(sort_by); - } - return query.limit(limit) - .skip(limit * page).updateMany({ $set: { "status": new_status } }) - .exec(); - //HERE CHECK IF EMAIL, and if so, then look @ status and see if its in the constants. - // IF SO then call the email service sendMany with all the emails of hackers from the query. - // NOT SURE IF WE SHOULD MAKE IT EXTENSIBLE, so have it where this gets passed a function, and function - // Is called with specific params selected from each hacker???? +/** + * @function executeStatusAction + * @param {string} model the model which is being searched + * @param {Array} queryArray array of clauses for the query + * @param {number} page the page number you want + * @param {number} limit the limit to the number of responses you want + * @param {"asc"|"desc"} sort which direction you want to sort by + * @param {string} sortBy the attribute you want to sort by + * @param {string} update the JSON string containing the keys and values to update to + * @returns {Promise<[Array]>} + * @description Builds and executes a status update based on a subset of mongodb + */ +function executeStatusAction(model, queryArray, page, limit, sort, sortBy, shouldExpand = false, update) { + var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); + var update_obj = JSON.parse(update); + return query.updateMany({ $set: update_obj }).exec(); +} - //Logic for 2nd option: - /* - pass function to here & object of select params that the function takes. - execute find query, with callback of: - - function(err, arr) ... if no err then loop through each hacker in arr. - - for every hacker call the function passed in with each appropriate params req. (! must be given in order) +/** + * @function executeEmailAction + * @param {string} model the model which is being searched + * @param {Array} queryArray array of clauses for the query + * @param {number} page the page number you want + * @param {number} limit the limit to the number of responses you want + * @param {"asc"|"desc"} sort which direction you want to sort by + * @param {string} sortBy the attribute you want to sort by + * @param {"Accepted"|"Waitlisted"|"Reminder"} status the status type of the email to send to hackers + * @returns {Promise<[Array]>} + * @description Sends a status update email based on a subset of mongodb + */ +function executeEmailAction(model, queryArray, page, limit, sort, sortBy, shouldExpand = false, status) { + var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); - */ + return query.exec().then(async (hackers) => { + console.log(hackers) + if (hackers) { + for (const hacker of hackers) { + const account = await AccountService.findById(hacker.accountId); + if (!account) { + break; + } + let x = await EmailService.sendStatusUpdateAsync(account.firstName, account.email, status) + console.log(x); + if (x) { + return "Email service failed." + } + } + } else { + return "Email service failed." + } + }).catch((err) => { + return err; + }); } module.exports = { executeQuery: executeQuery, - executeAction: executeAction + executeStatusAction: executeStatusAction, + executeEmailAction: executeEmailAction }; \ No newline at end of file