From a96c1d9c07b5045faf8c908625d096a80f37ac3a Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 4 Jan 2021 20:55:02 +0100 Subject: [PATCH] Add seach plugin for products --- src/controllers/product.controller.js | 12 ++++-- src/controllers/profile.controller.js | 2 +- src/models/plugins/index.js | 1 + src/models/plugins/paginate.plugin.js | 17 -------- src/models/plugins/search.plugin.js | 18 ++++++++ src/models/plugins/toJSON.plugin.js | 5 --- src/models/product.model.js | 7 ++-- src/models/profile.model.js | 21 +++++++--- src/routes/v1/product.route.js | 4 ++ src/routes/v1/profile.route.js | 2 +- src/seeders/product.seeder.js | 60 ++++++++++++++++++++++++++- src/services/product.service.js | 10 ++++- src/services/profile.service.js | 28 +++++++++---- src/validations/profile.validation.js | 42 +++++++++++++------ 14 files changed, 170 insertions(+), 59 deletions(-) create mode 100644 src/models/plugins/search.plugin.js diff --git a/src/controllers/product.controller.js b/src/controllers/product.controller.js index 5ad5ac1..7a00e22 100644 --- a/src/controllers/product.controller.js +++ b/src/controllers/product.controller.js @@ -10,9 +10,9 @@ const createProduct = catchAsync(async (req, res) => { }); const getProducts = catchAsync(async (req, res) => { - const filter = pick(req.query, ['label', 'barcode']); - const options = pick(req.query, ['sortBy', 'limit', 'page']); - const result = await productService.queryProducts(filter, options); + const query = pick(req.query, ['label', 'barcode']); + const options = pick(req.query, ['limit']); + const result = await productService.queryProducts(query, options); res.send(result); }); @@ -34,10 +34,16 @@ const deleteProduct = catchAsync(async (req, res) => { res.status(httpStatus.NO_CONTENT).send(); }); +const getAllProducts = catchAsync(async (req, res) => { + const products = await productService.getAllProducts(); + res.send(products); +}); + module.exports = { createProduct, getProducts, getProduct, updateProduct, deleteProduct, + getAllProducts, }; diff --git a/src/controllers/profile.controller.js b/src/controllers/profile.controller.js index 48c1314..69bf4a4 100644 --- a/src/controllers/profile.controller.js +++ b/src/controllers/profile.controller.js @@ -5,7 +5,7 @@ const catchAsync = require('../utils/catchAsync'); const { profileService } = require('../services'); const createProfile = catchAsync(async (req, res) => { - req.body.userId = req.user._id; + req.body.user = req.user._id; const profile = await profileService.createProfile(req.body); res.status(httpStatus.CREATED).send(profile); }); diff --git a/src/models/plugins/index.js b/src/models/plugins/index.js index d85ebb8..de5d2ae 100644 --- a/src/models/plugins/index.js +++ b/src/models/plugins/index.js @@ -1,2 +1,3 @@ module.exports.toJSON = require('./toJSON.plugin'); module.exports.paginate = require('./paginate.plugin'); +module.exports.search = require('./search.plugin'); diff --git a/src/models/plugins/paginate.plugin.js b/src/models/plugins/paginate.plugin.js index 392ccd5..4550104 100644 --- a/src/models/plugins/paginate.plugin.js +++ b/src/models/plugins/paginate.plugin.js @@ -1,23 +1,6 @@ /* eslint-disable no-param-reassign */ const paginate = (schema) => { - /** - * @typedef {Object} QueryResult - * @property {Document[]} results - Results found - * @property {number} page - Current page - * @property {number} limit - Maximum number of results per page - * @property {number} totalPages - Total number of pages - * @property {number} totalResults - Total number of documents - */ - /** - * Query for documents with pagination - * @param {Object} [filter] - Mongo filter - * @param {Object} [options] - Query options - * @param {string} [options.sortBy] - Sorting criteria using the format: sortField:(desc|asc). Multiple sorting criteria should be separated by commas (,) - * @param {number} [options.limit] - Maximum number of results per page (default = 10) - * @param {number} [options.page] - Current page (default = 1) - * @returns {Promise} - */ schema.statics.paginate = async function (filter, options) { let sort = ''; if (options.sortBy) { diff --git a/src/models/plugins/search.plugin.js b/src/models/plugins/search.plugin.js new file mode 100644 index 0000000..3248bf4 --- /dev/null +++ b/src/models/plugins/search.plugin.js @@ -0,0 +1,18 @@ +/* eslint-disable no-param-reassign */ + +const search = (schema) => { + schema.statics.search = async function (query, options) { + const [[key, pattern]] = Object.entries(query); + + const searchRegex = Number.isInteger(pattern) ? pattern : new RegExp(`.*${pattern}.*`); + const limit = options.limit && parseInt(options.limit, 10) > 0 ? parseInt(options.limit, 10) : 10; + + const docsPromise = this.find({ [key]: searchRegex }) + .limit(limit) + .exec(); + + return Promise.resolve(docsPromise); + }; +}; + +module.exports = search; diff --git a/src/models/plugins/toJSON.plugin.js b/src/models/plugins/toJSON.plugin.js index bb7abda..ae16ba8 100644 --- a/src/models/plugins/toJSON.plugin.js +++ b/src/models/plugins/toJSON.plugin.js @@ -1,10 +1,5 @@ /* eslint-disable no-param-reassign */ -/** - * A mongoose schema plugin which applies the following in the toJSON transform call: - * - removes __v, createdAt, updatedAt, and any path that has private: true - * - replaces _id with id - */ const toJSON = (schema) => { let transform; if (schema.options.toJSON && schema.options.toJSON.transform) { diff --git a/src/models/product.model.js b/src/models/product.model.js index ddd53b8..f824b87 100644 --- a/src/models/product.model.js +++ b/src/models/product.model.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const { toJSON, paginate } = require('./plugins'); +const { toJSON, paginate, search } = require('./plugins'); const productSchema = mongoose.Schema( { @@ -65,9 +65,10 @@ const productSchema = mongoose.Schema( productSchema.plugin(toJSON); productSchema.plugin(paginate); +productSchema.plugin(search); -productSchema.statics.isNameTaken = async function (name, excludeProductId) { - const product = await this.findOne({ name, _id: { $ne: excludeProductId } }); +productSchema.statics.isNameTaken = async function (label, excludeProductId) { + const product = await this.findOne({ label, _id: { $ne: excludeProductId } }); return !!product; }; diff --git a/src/models/profile.model.js b/src/models/profile.model.js index feed9a9..8607d96 100644 --- a/src/models/profile.model.js +++ b/src/models/profile.model.js @@ -1,16 +1,27 @@ const mongoose = require('mongoose'); const { toJSON, paginate } = require('./plugins'); +const { genderTypes } = require('../config/genders'); +const { goalTypes } = require('../config/goals'); +const { activityTypes } = require('../config/activities'); const profileSchema = mongoose.Schema( { gender: { type: String, - enum: ['male', 'female'], + enum: Object.values(genderTypes), required: true, }, goal: { type: String, - enum: ['lose_weight', 'maintain_weight', 'put_on_weight'], + enum: Object.values(goalTypes), + required: true, + }, + dailyCalories: { + type: Number, + required: true, + }, + weeksToGoal: { + type: Number, required: true, }, birthday: { @@ -36,10 +47,10 @@ const profileSchema = mongoose.Schema( }, activity: { type: Number, - enum: [1.2, 1.375, 1.55, 1.725, 1.9], + enum: Object.values(activityTypes), required: true, }, - userId: { + user: { type: mongoose.SchemaTypes.ObjectId, ref: 'User', required: true, @@ -55,7 +66,7 @@ profileSchema.plugin(toJSON); profileSchema.plugin(paginate); profileSchema.statics.hasUser = async function (userId, excludeProfileId) { - const user = await this.findOne({ userId, _id: { $ne: excludeProfileId } }); + const user = await this.findOne({ user: userId, _id: { $ne: excludeProfileId } }); return !!user; }; diff --git a/src/routes/v1/product.route.js b/src/routes/v1/product.route.js index e31a3dc..c101515 100644 --- a/src/routes/v1/product.route.js +++ b/src/routes/v1/product.route.js @@ -11,6 +11,10 @@ router .post(auth(), validate(productValidation.createProduct), productController.createProduct) .get(auth(), validate(productValidation.getProducts), productController.getProducts); +router + .route('/all') + .get(productController.getAllProducts) + router .route('/:productId') .get(auth(), validate(productValidation.getProduct), productController.getProduct) diff --git a/src/routes/v1/profile.route.js b/src/routes/v1/profile.route.js index f6baa13..c12207f 100644 --- a/src/routes/v1/profile.route.js +++ b/src/routes/v1/profile.route.js @@ -14,7 +14,7 @@ router .delete(auth(), validate(profileValidation.deleteProfile), profileController.deleteProfile); router - .route('/admin') + .route('/all') .get(auth(), validate(profileValidation.getProfiles), profileController.getProfiles); module.exports = router; diff --git a/src/seeders/product.seeder.js b/src/seeders/product.seeder.js index 87d3c40..667e20a 100644 --- a/src/seeders/product.seeder.js +++ b/src/seeders/product.seeder.js @@ -3,7 +3,7 @@ const { Product } = require('../models'); const data = [ { - label: 'cappy - multiwitamina', + label: 'multiwitamina', brand: 'capy', verified: true, eco: false, @@ -17,7 +17,7 @@ const data = [ salt: 0, }, { - label: 'star - chipsy o smaku smietana i cebula', + label: 'chipsy o smaku smietana i cebula', brand: 'star', verified: true, eco: false, @@ -30,6 +30,62 @@ const data = [ protein: 6.2, salt: 1.4, }, + { + label: 'Aloe', + brand: 'tropical', + verified: true, + eco: false, + barcode: 8712857004606, + unit: 'ml', + servingCapacity: 500, + calories: 20, + fat: 0, + carbohydrates: 4.9, + protein: 0, + salt: 0.042, + }, + { + label: `chocolate m&m's`, + brand: `m&m's`, + verified: true, + eco: false, + barcode: 5000159471725, + unit: 'g', + servingCapacity: 150, + calories: 481, + fat: 19, + carbohydrates: 71, + protein: 5, + salt: 0.13, + }, + { + label: `black`, + brand: `black`, + verified: true, + eco: false, + barcode: 5900552014713, + unit: 'ml', + servingCapacity: 250, + calories: 42, + fat: 0, + carbohydrates: 9.9, + protein: 0, + salt: 0.18, + }, + { + label: `fantasia mleczna`, + brand: `e.wedel`, + verified: true, + eco: false, + barcode: 59074571, + unit: 'g', + servingCapacity: 104, + calories: 166, + fat: 9.3, + carbohydrates: 16.9, + protein: 0.3, + salt: 0.14, + }, ]; class ProductSeeder extends Seeder { diff --git a/src/services/product.service.js b/src/services/product.service.js index 35969ef..9b4e46a 100644 --- a/src/services/product.service.js +++ b/src/services/product.service.js @@ -13,8 +13,8 @@ const createProduct = async (productBody) => { return product; }; -const queryProducts = async (filter, options) => { - const products = await Product.paginate(filter, options); +const queryProducts = async (query, options) => { + const products = await Product.search(query, options); return products; }; @@ -45,6 +45,11 @@ const deleteProductById = async (productId) => { return product; }; +const getAllProducts = async () => { + const products = await Product.find(); + return products; +}; + module.exports = { createProduct, queryProducts, @@ -52,4 +57,5 @@ module.exports = { getProductByBarcode, updateProductById, deleteProductById, + getAllProducts, }; diff --git a/src/services/profile.service.js b/src/services/profile.service.js index 1c78bda..c1028d8 100644 --- a/src/services/profile.service.js +++ b/src/services/profile.service.js @@ -5,10 +5,18 @@ const calculateWeeksToGetGoalWeight = require('../utils/weeksToGetGoalWeight'); const dailyCaloricRequirement = require('../utils/dailyCaloricRequirement'); const createProfile = async (profileBody) => { - if (await Profile.hasUser(profileBody.userId)) { + if (await Profile.hasUser(profileBody.user)) { throw new ApiError(httpStatus.BAD_REQUEST, 'User already has a profile'); } - const profile = await Profile.create(profileBody); + + const dailyCalories = dailyCaloricRequirement(profileBody); + const weeksToGoal = calculateWeeksToGetGoalWeight(profileBody); + + const profile = await Profile.create({ + ...profileBody, + dailyCalories, + weeksToGoal, + }); return profile; }; @@ -19,13 +27,11 @@ const queryProfiles = async (filter, options) => { const getProfileById = async (id) => { const profile = await Profile.findById(id); - console.log(dailyCaloricRequirement(profile)); - console.log(calculateWeeksToGetGoalWeight(profile)); - return profile + return profile; }; const getProfileByUserId = async (userId) => { - return Profile.findOne({ userId }); + return Profile.findOne({ user: userId }); }; const updateProfileById = async (profileId, updateBody) => { @@ -33,7 +39,15 @@ const updateProfileById = async (profileId, updateBody) => { if (!profile) { throw new ApiError(httpStatus.NOT_FOUND, 'Profile not found'); } - Object.assign(profile, updateBody); + + const dailyCalories = dailyCaloricRequirement(updateBody); + const weeksToGoal = calculateWeeksToGetGoalWeight(updateBody); + + Object.assign(profile, { + ...updateBody, + dailyCalories, + weeksToGoal, + }); await profile.save(); return profile; }; diff --git a/src/validations/profile.validation.js b/src/validations/profile.validation.js index c7d6fc1..5f7bfe2 100644 --- a/src/validations/profile.validation.js +++ b/src/validations/profile.validation.js @@ -6,14 +6,22 @@ const { activityTypes } = require('../config/activities'); const createProfile = { body: Joi.object().keys({ - gender: Joi.string().required().valid(Object.values(genderTypes)), - goal: Joi.string().required().valid(Object.values(goalTypes)), + gender: Joi.string() + .valid(...Object.values(genderTypes)) + .required(), + goal: Joi.string() + .valid(...Object.values(goalTypes)) + .required(), + dailyCalories: Joi.number(), + weeksToGoal: Joi.number(), birthday: Joi.date().required(), height: Joi.number().required(), currentWeight: Joi.number().required(), goalWeight: Joi.number().required(), rateOfChange: Joi.number().required(), - activity: Joi.number().required().valid(Object.values(activityTypes)), + activity: Joi.number() + .valid(...Object.values(activityTypes)) + .required(), }), }; @@ -35,16 +43,24 @@ const updateProfile = { params: Joi.object().keys({ profileId: Joi.required().custom(objectId), }), - body: Joi.object().keys({ - gender: Joi.string().required().valid(Object.values(genderTypes)), - goal: Joi.string().required().valid(Object.values(goalTypes)), - birthday: Joi.date().required(), - height: Joi.number().required(), - currentWeight: Joi.number().required(), - goalWeight: Joi.number().required(), - rateOfChange: Joi.number(), - activity: Joi.number().required().valid(Object.values(activityTypes)), - }).min(1), + body: Joi.object() + .keys({ + gender: Joi.string() + .valid(...Object.values(genderTypes)) + .required(), + goal: Joi.string() + .valid(...Object.values(goalTypes)) + .required(), + birthday: Joi.date().required(), + height: Joi.number().required(), + currentWeight: Joi.number().required(), + goalWeight: Joi.number().required(), + rateOfChange: Joi.number(), + activity: Joi.number() + .required() + .valid(...Object.values(activityTypes)), + }) + .min(1), }; const deleteProfile = {