From 04932d199d96d0879f196a8ad6265bb6d8294797 Mon Sep 17 00:00:00 2001 From: Pierre Theo Klein Date: Fri, 1 Feb 2019 23:27:04 -0500 Subject: [PATCH 1/7] version 1.6.1 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index ce6a70b9..2eda823f 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.0 \ No newline at end of file +1.6.1 \ No newline at end of file From 1bee227db0872440cfd565b4316f2e390e31c48b Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Sat, 26 Oct 2019 16:15:22 -0400 Subject: [PATCH 2/7] Works for batch status updates --- constants/general.constant.js | 12 ++++ constants/routes.constant.js | 6 +- middlewares/search.middleware.js | 25 ++++++- middlewares/validators/search.validator.js | 12 ++++ middlewares/validators/validator.helper.js | 54 ++++++++++++--- routes/api/search.js | 50 ++++++++++++++ services/search.service.js | 76 +++++++++++++++++++++- 7 files changed, 222 insertions(+), 13 deletions(-) diff --git a/constants/general.constant.js b/constants/general.constant.js index 01267922..421b5e8d 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -23,6 +23,16 @@ const HACKER_STATUSES = [ HACKER_STATUS_CANCELLED, HACKER_STATUS_CHECKED_IN ]; +const VALID_SEARCH_ACTIONS = [ + "change_status", + "email", + "change_status_and_email" +]; + +const CORRESPONDING_STATUSES = { + "change_status": HACKER_STATUSES, + "email": ["Acceptance", "Waitlist", "Reminder"] +} const SAMPLE_DIET_RESTRICTIONS = [ "None", @@ -151,4 +161,6 @@ module.exports = { MAX_TEAM_SIZE: MAX_TEAM_SIZE, WEEK_OF: WEEK_OF, SAMPLE_DIET_RESTRICTIONS: SAMPLE_DIET_RESTRICTIONS, + VALID_SEARCH_ACTIONS: VALID_SEARCH_ACTIONS, + CORRESPONDING_STATUSES: CORRESPONDING_STATUSES }; \ No newline at end of file diff --git a/constants/routes.constant.js b/constants/routes.constant.js index d7b4f799..4be1d490 100644 --- a/constants/routes.constant.js +++ b/constants/routes.constant.js @@ -214,7 +214,11 @@ const searchRoutes = { "get": { requestType: Constants.REQUEST_TYPES.GET, uri: "/api/search/" - } + }, + "bactchAction": { + requestType: Constants.REQUEST_TYPES.GET, + uri: "/api/search/action", + }, }; const staffRoutes = { diff --git a/middlewares/search.middleware.js b/middlewares/search.middleware.js index b6707568..8572a6fa 100644 --- a/middlewares/search.middleware.js +++ b/middlewares/search.middleware.js @@ -15,7 +15,6 @@ const Middleware = { */ function parseQuery(req, res, next) { let query = req.body.q; - req.body.q = JSON.parse(query); //Default page @@ -62,6 +61,29 @@ async function executeQuery(req, res, next) { return next(); } +/** + * + * @param {} req + * @param {*} res + * @param {*} 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.q, + req.body.page, + req.body.limit, + req.body.sort, + req.body.sort_by, + req.body.expand, + req.body.status + ); + return next(); +} + function setExpandTrue(req, res, next) { req.body.expand = true; next(); @@ -71,5 +93,6 @@ function setExpandTrue(req, res, next) { module.exports = { parseQuery: parseQuery, executeQuery: Middleware.Util.asyncMiddleware(executeQuery), + executeStatusAction: Middleware.Util.asyncMiddleware(executeStatusAction), setExpandTrue: setExpandTrue, }; \ No newline at end of file diff --git a/middlewares/validators/search.validator.js b/middlewares/validators/search.validator.js index 837c42ed..0ca231a7 100644 --- a/middlewares/validators/search.validator.js +++ b/middlewares/validators/search.validator.js @@ -11,4 +11,16 @@ module.exports = { VALIDATOR.booleanValidator("query", "expand", true), VALIDATOR.searchValidator("query", "q") ], + searchActionValidator: [ + 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"), + /* ACTION VALIDATOR NEED TO MAKE WORK! */ + VALIDATOR.actionValidator("query", "action"), + 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 1ebcf272..fe387737 100644 --- a/middlewares/validators/validator.helper.js +++ b/middlewares/validators/validator.helper.js @@ -27,8 +27,8 @@ function integerValidator(fieldLocation, fieldname, optional = true, lowerBound if (optional) { return value.optional({ - checkFalsy: true - }) + checkFalsy: true + }) .isInt().withMessage(`${fieldname} must be an integer.`) .custom((value) => { return value >= lowerBound && value <= upperBound; @@ -71,8 +71,8 @@ function mongoIdArrayValidator(fieldLocation, fieldname, optional = true) { if (optional) { return arr.optional({ - checkFalsy: true - }) + checkFalsy: true + }) .custom(isMongoIdArray).withMessage("Value must be an array of mongoIDs"); } else { return arr.exists() @@ -137,8 +137,8 @@ function regexValidator(fieldLocation, fieldname, optional = true, desire = Cons if (optional) { return match.optional({ - checkFalsy: true - }) + checkFalsy: true + }) .matches(desire) .withMessage("must be valid url"); } else { @@ -315,8 +315,8 @@ function jwtValidator(fieldLocation, fieldname, jwtSecret, optional = true) { const jwtValidationChain = setProperValidationChainBuilder(fieldLocation, fieldname, "Must be vali jwt"); if (optional) { return jwtValidationChain.optional({ - checkFalsy: true - }) + checkFalsy: true + }) .custom(value => { const token = jwt.verify(value, jwtSecret); if (typeof token !== "undefined") { @@ -406,8 +406,8 @@ function searchValidator(fieldLocation, fieldname) { function searchSortValidator(fieldLocation, fieldName) { const searchSort = setProperValidationChainBuilder(fieldLocation, fieldName, "Invalid sort criteria") return searchSort.optional({ - checkFalsy: true - }) + checkFalsy: true + }) .custom((value, { req }) => { @@ -551,6 +551,38 @@ function enumValidator(fieldLocation, fieldname, enums, optional = true) { } } +/** + * 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 actionValidator(fieldLocation, actionFieldName) { + const actionValue = setProperValidationChainBuilder(fieldLocation, actionFieldName, "Invalid action."); + + return actionValue.exists() + .withMessage("The action must exist.") + .custom(actionValidatorHelper).withMessage("The value must be a valid action."); +} + + +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); + }).withMessage("The value must be a proper status.") +} + +function actionValidatorHelper(action) { + if (Constants.VALID_SEARCH_ACTIONS.includes(action)) { + return true; + } + return false; +} + + /** * Checks that 'value' is part of 'enums'. 'enums' should be an enum dict. * @param {*} value Should be of the same type as the values of the enum @@ -614,4 +646,6 @@ module.exports = { dateValidator: dateValidator, enumValidator: enumValidator, routesValidator: routesValidator, + actionValidator: actionValidator, + statusValidator: statusValidator }; \ No newline at end of file diff --git a/routes/api/search.js b/routes/api/search.js index 9d1a4253..98416823 100644 --- a/routes/api/search.js +++ b/routes/api/search.js @@ -67,6 +67,56 @@ module.exports = { Controllers.Search.searchResults ); + /** + * @api {get} /search/action execute an action on a specific query for any defined model + * @apiName search + * @apiGroup Search + * @apiVersion 0.0.8 + * + * @apiParam (query) {String} model the model to be searched + * @apiParam (query) {Array} q the query to be executed. For more information on how to format this, please see https://docs.mchacks.ca/architecture/ + * @apiParam (query) {String} model the model to be searched + * @apiParam (query) {String} sort either "asc" or "desc" + * @apiParam (query) {number} page the page number that you would like + * @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 + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Results + * @apiSuccessExample {object} Success-Response: + * { + "message": "Successfully executed query, returning all results", + "data": [ + {...} + ] + } + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Empty object + * @apiSuccessExample {object} Success-Response: + * { + "message": "No results found.", + "data": {} + } + * + * @apiError {String} message Error message + * @apiError {Object} data empty + * @apiErrorExample {object} Error-Response: + * {"message": "Validation failed", "data": {}} + */ + searchRouter.route("/action").get( + Middleware.Auth.ensureAuthenticated(), + Middleware.Auth.ensureAuthorized(), + Middleware.Validator.Search.searchActionValidator, + Middleware.parseBody.middleware, + Middleware.Search.parseQuery, + Middleware.Search.executeStatusAction, + Controllers.Search.searchResults + ); + apiRouter.use("/search", searchRouter); } }; \ No newline at end of file diff --git a/services/search.service.js b/services/search.service.js index 597a4cb1..77467768 100644 --- a/services/search.service.js +++ b/services/search.service.js @@ -72,6 +72,80 @@ function executeQuery(model, queryArray, page, limit, sort, sort_by, shouldExpan .exec(); } + +/** + * @function executeAction + * @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 + * @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; + } + } + + 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(); +} + module.exports = { - executeQuery: executeQuery + executeQuery: executeQuery, + executeAction: executeAction }; \ No newline at end of file From d8b634af5cc03b607ccde2ad46c93c0f8e0e5d59 Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Tue, 29 Oct 2019 14:22:27 -0400 Subject: [PATCH 3/7] Constants + ToDo, email on local --- middlewares/search.middleware.js | 1 + services/search.service.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/middlewares/search.middleware.js b/middlewares/search.middleware.js index 8572a6fa..f40ef915 100644 --- a/middlewares/search.middleware.js +++ b/middlewares/search.middleware.js @@ -5,6 +5,7 @@ const Services = { const Middleware = { Util: require("../middlewares/util.middleware") } +const Constants = require("../../constants/general.constant"); /** * @function parseQuery diff --git a/services/search.service.js b/services/search.service.js index 77467768..71a787b2 100644 --- a/services/search.service.js +++ b/services/search.service.js @@ -143,6 +143,21 @@ function executeAction(model, queryArray, page, limit, sort, sort_by, shouldExpa 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???? + + + + //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) + + */ } module.exports = { From b135286cb0ff049e1f4f7555e1c900b018770e6d Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Thu, 31 Oct 2019 19:15:53 -0400 Subject: [PATCH 4/7] Finished email --- constants/routes.constant.js | 8 +- constants/success.constant.js | 2 + controllers/search.controller.js | 19 ++- middlewares/search.middleware.js | 27 +++- middlewares/validators/search.validator.js | 16 ++- middlewares/validators/validator.helper.js | 36 ++++- routes/api/search.js | 19 ++- services/email.service.js | 25 +++- services/search.service.js | 150 ++++++++++----------- 9 files changed, 200 insertions(+), 102 deletions(-) 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 From c6c82342ff69adc561370419c4c249ec2aad93b4 Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Sun, 3 Nov 2019 14:46:52 -0500 Subject: [PATCH 5/7] Formatting changes --- controllers/search.controller.js | 2 +- middlewares/search.middleware.js | 8 ++--- middlewares/validators/search.validator.js | 2 +- middlewares/validators/validator.helper.js | 16 +++++----- services/search.service.js | 37 ++++++++++------------ 5 files changed, 30 insertions(+), 35 deletions(-) diff --git a/controllers/search.controller.js b/controllers/search.controller.js index 0322da7a..b144e477 100644 --- a/controllers/search.controller.js +++ b/controllers/search.controller.js @@ -25,7 +25,7 @@ async function searchResults(req, res) { async function emailResults(req, res) { let results = req.body.results; let message; - if (results == undefined) { + if (results === undefined) { message = Success.HACKER_UPDATE_EMAILS; results = {} } else { diff --git a/middlewares/search.middleware.js b/middlewares/search.middleware.js index 94c39b96..e19b2b97 100644 --- a/middlewares/search.middleware.js +++ b/middlewares/search.middleware.js @@ -76,8 +76,8 @@ async function executeStatusAction(req, res, next) { req.body.limit, req.body.sort, req.body.sort_by, - req.body.expand, - req.body.update + req.body.update, + req.body.expand ); return next(); } @@ -97,8 +97,8 @@ async function executeEmailAction(req, res, next) { req.body.limit, req.body.sort, req.body.sort_by, - req.body.expand, - req.body.status + req.body.status, + req.body.expand ); return next(); } diff --git a/middlewares/validators/search.validator.js b/middlewares/validators/search.validator.js index 4e238b7b..75f3ed93 100644 --- a/middlewares/validators/search.validator.js +++ b/middlewares/validators/search.validator.js @@ -19,7 +19,7 @@ module.exports = { VALIDATOR.searchSortValidator("query", "sort_by"), VALIDATOR.booleanValidator("query", "expand", true), VALIDATOR.searchValidator("query", "q"), - VALIDATOR.updateObjectValidator("query", "update") + VALIDATOR.updateHackerValidator("query", "update") ], emailValidator: [ VALIDATOR.searchModelValidator("query", "model", false), diff --git a/middlewares/validators/validator.helper.js b/middlewares/validators/validator.helper.js index db96f2b0..3c3bed24 100644 --- a/middlewares/validators/validator.helper.js +++ b/middlewares/validators/validator.helper.js @@ -567,20 +567,20 @@ function actionValidator(fieldLocation, actionFieldName) { /** - * Validates that action field is a valid action from constants passed, and checks if corresponding new status is valid. + * Validates that the field is a valid hacker update object, 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."); +function updateHackerValidator(fieldLocation, fieldName) { + const hackerObjectValue = setProperValidationChainBuilder(fieldLocation, fieldName, "Invalid hacker update object string."); - return updateObjectValue.exists() - .withMessage("The update object string must exist.") - .custom(updateObjectValidatorHelper).withMessage("The value must be a valid update object."); + return hackerObjectValue.exists() + .withMessage("The hacker update object string must exist.") + .custom(updateHackerValidatorHelper).withMessage("The value must be a valid hacker update object."); } -function updateObjectValidatorHelper(update) { +function updateHackerValidatorHelper(update) { try { var updateObject = JSON.parse(update); @@ -679,5 +679,5 @@ module.exports = { routesValidator: routesValidator, actionValidator: actionValidator, statusValidator: statusValidator, - updateObjectValidator: updateObjectValidator, + updateHackerValidator: updateHackerValidator, }; \ No newline at end of file diff --git a/services/search.service.js b/services/search.service.js index da69f30d..36f33cbd 100644 --- a/services/search.service.js +++ b/services/search.service.js @@ -85,7 +85,7 @@ function createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand */ function executeQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand = false) { var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); - return query.exec(); + return query.exec('find'); } @@ -101,7 +101,7 @@ function executeQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand * @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) { +function executeStatusAction(model, queryArray, page, limit, sort, sortBy, update, shouldExpand = false) { var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); var update_obj = JSON.parse(update); return query.updateMany({ $set: update_obj }).exec(); @@ -120,29 +120,24 @@ function executeStatusAction(model, queryArray, page, limit, sort, sortBy, shoul * @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) { +async function executeEmailAction(model, queryArray, page, limit, sort, sortBy, status, shouldExpand = false) { var query = createQuery(model, queryArray, page, limit, sort, sortBy, shouldExpand); + const hackers = await query.exec() - 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." - } + if (hackers) { + for (const hacker of hackers) { + const account = await AccountService.findById(hacker.accountId); + if (!account) { + break; + } + let emailError = await EmailService.sendStatusUpdateAsync(account.firstName, account.email, status) + if (emailError) { + return "Email service failed." } - } else { - return "Email service failed." } - }).catch((err) => { - return err; - }); + } else { + return "Email service failed." + } } module.exports = { From 32d7ecab0dec71ebb092fc56ede98b489220e620 Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Sun, 3 Nov 2019 14:57:05 -0500 Subject: [PATCH 6/7] Changed comments --- routes/api/search.js | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/routes/api/search.js b/routes/api/search.js index bdf680b8..3f565563 100644 --- a/routes/api/search.js +++ b/routes/api/search.js @@ -80,8 +80,8 @@ module.exports = { * @apiParam (query) {number} page the page number that you would like * @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) {String} json object string containing the update fields for the defined model * @apiParam (query) {boolean} expand whether you want to expand sub documents within the results - * @apiParam (query) {String} update new status or type of email * * @apiSuccess {String} message Success message * @apiSuccess {Object} data Results @@ -116,6 +116,38 @@ module.exports = { Controllers.Search.searchResults ); + /** + * @api {get} /search/sendEmails execute an action on a specific query for any defined model + * @apiName search + * @apiGroup Search + * @apiVersion 0.0.8 + * + * @apiParam (query) {String} model the model to be searched + * @apiParam (query) {Array} q the query to be executed. For more information on how to format this, please see https://docs.mchacks.ca/architecture/ + * @apiParam (query) {String} model the model to be searched + * @apiParam (query) {String} sort either "asc" or "desc" + * @apiParam (query) {number} page the page number that you would like + * @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) {String} | + * @apiParam (query) {boolean} expand whether you want to expand sub documents within the results + * + * @apiSuccess {String} message Success message + * @apiSuccess {Object} data Results + * @apiSuccessExample {object} Success-Response: + * { + "message": "Hacker update emails sent.", + "data": {} + } + * + * @apiSuccess {String} message Error message + * @apiSuccess {Object} data Specific Error message + * @apiSuccessExample {object} Success-Response: + * { + "message": "Error while generating email", + "data": "Email service failed." + } + */ searchRouter.route("/sendEmails").get( Middleware.Auth.ensureAuthenticated(), Middleware.Auth.ensureAuthorized(), From be46e7aa93f4baf426143141eb9e48752596222c Mon Sep 17 00:00:00 2001 From: Harsh Patel Date: Sun, 3 Nov 2019 15:00:40 -0500 Subject: [PATCH 7/7] Removed unnecessary functions added --- constants/general.constant.js | 12 ----------- middlewares/validators/validator.helper.js | 24 ---------------------- 2 files changed, 36 deletions(-) diff --git a/constants/general.constant.js b/constants/general.constant.js index 421b5e8d..01267922 100644 --- a/constants/general.constant.js +++ b/constants/general.constant.js @@ -23,16 +23,6 @@ const HACKER_STATUSES = [ HACKER_STATUS_CANCELLED, HACKER_STATUS_CHECKED_IN ]; -const VALID_SEARCH_ACTIONS = [ - "change_status", - "email", - "change_status_and_email" -]; - -const CORRESPONDING_STATUSES = { - "change_status": HACKER_STATUSES, - "email": ["Acceptance", "Waitlist", "Reminder"] -} const SAMPLE_DIET_RESTRICTIONS = [ "None", @@ -161,6 +151,4 @@ module.exports = { MAX_TEAM_SIZE: MAX_TEAM_SIZE, WEEK_OF: WEEK_OF, SAMPLE_DIET_RESTRICTIONS: SAMPLE_DIET_RESTRICTIONS, - VALID_SEARCH_ACTIONS: VALID_SEARCH_ACTIONS, - CORRESPONDING_STATUSES: CORRESPONDING_STATUSES }; \ No newline at end of file diff --git a/middlewares/validators/validator.helper.js b/middlewares/validators/validator.helper.js index 3c3bed24..e14c8105 100644 --- a/middlewares/validators/validator.helper.js +++ b/middlewares/validators/validator.helper.js @@ -551,21 +551,6 @@ function enumValidator(fieldLocation, fieldname, enums, optional = true) { } } -/** - * 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 actionValidator(fieldLocation, actionFieldName) { - const actionValue = setProperValidationChainBuilder(fieldLocation, actionFieldName, "Invalid action."); - - return actionValue.exists() - .withMessage("The action must exist.") - .custom(actionValidatorHelper).withMessage("The value must be a valid action."); -} - - /** * Validates that the field is a valid hacker update object, and checks if corresponding new status is valid. * @param {"query" | "body" | "header" | "param"} fieldLocation The location where the field should be found. @@ -606,14 +591,6 @@ function statusValidator(fieldLocation, statusFieldName) { }).withMessage("The value must be a proper status.") } -function actionValidatorHelper(action) { - if (Constants.VALID_SEARCH_ACTIONS.includes(action)) { - return true; - } - return false; -} - - /** * Checks that 'value' is part of 'enums'. 'enums' should be an enum dict. * @param {*} value Should be of the same type as the values of the enum @@ -677,7 +654,6 @@ module.exports = { dateValidator: dateValidator, enumValidator: enumValidator, routesValidator: routesValidator, - actionValidator: actionValidator, statusValidator: statusValidator, updateHackerValidator: updateHackerValidator, }; \ No newline at end of file