From d17a10a46ea2f60529ca4c10a47f06348762459a Mon Sep 17 00:00:00 2001 From: Vivek Kumar Date: Mon, 18 Nov 2024 23:34:15 +0530 Subject: [PATCH] Add Product endpoints and controller for simple products --- constants/models.constants.js | 4 +- constants/regex.constants.js | 3 + controllers/product.controller.js | 209 ++++++++++++++++++++++++++++++ index.js | 1 + middleware/multer.middleware.js | 2 - model/category.model.js | 2 +- model/product.model.js | 21 +-- routes/product.routes.js | 34 +++++ 8 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 controllers/product.controller.js create mode 100644 routes/product.routes.js diff --git a/constants/models.constants.js b/constants/models.constants.js index 6cf16a6..e8b772e 100644 --- a/constants/models.constants.js +++ b/constants/models.constants.js @@ -36,8 +36,8 @@ module.exports = { PUBLISHED: "PUBLISHED", DRAFT: "DRAFT", - SIMPLE: "simple", - VARIABLE: "variable", + SIMPLE: "SIMPLE", + VARIABLE: "VARIABLE", MONGOOSE_DUPLICATE_KEY: 11000, MONGOOSE_VALIDATION_ERROR: "ValidationError", diff --git a/constants/regex.constants.js b/constants/regex.constants.js index 7f64405..0d46795 100644 --- a/constants/regex.constants.js +++ b/constants/regex.constants.js @@ -17,6 +17,8 @@ const addressRegex = const pincodeRegex = /^[A-Za-z0-9\s-]{3,10}$/; +const productNameRegex = /^[a-zA-Z0-9\s\-,:'./()&‑]+$/; + module.exports = { emailRegex, usernameRegex, @@ -26,4 +28,5 @@ module.exports = { addressRegex, cityRegex, pincodeRegex, + productNameRegex, }; diff --git a/controllers/product.controller.js b/controllers/product.controller.js new file mode 100644 index 0000000..8caaec2 --- /dev/null +++ b/controllers/product.controller.js @@ -0,0 +1,209 @@ +const { + SIMPLE, + MONGOOSE_DUPLICATE_KEY, + MONGOOSE_VALIDATION_ERROR, + MONGOOSE_CAST_ERROR, +} = require("../constants/models.constants"); +const Product = require("../model/product.model"); +const invalidFieldMessage = require("../utils/invalidFieldMessage"); +const asyncHandler = require("../utils/asyncHandler"); +const ApiResponse = require("../utils/ApiResponse"); +const Category = require("../model/category.model"); +const Tag = require("../model/tag.model"); + +const addAProduct = asyncHandler(async (req, res) => { + try { + const { + name, + slug, + productType, + description, + shortDescription, + productStatus, + publishDate, + } = req.body; + + let { categories, tags } = req.body; + + const attribute = productType === SIMPLE ? req.body.attribute : null; + + const thumbnail = req.files.thumbnail[0].filename; + const gallery = req.files.gallery.map((file) => file.filename); + + categories = categories.split(","); + tags = tags.split(","); + + // Resolve categories: can't use map because it doesn't wait and hence we get promise pending + const addedCategories = await Promise.all( + categories.map(async (category) => { + let productCategory = await Category.findOne({ name: category }); + if (!productCategory) { + productCategory = await Category.create({ name: category }); // Ensure await here + } + return productCategory._id; + }) + ); + + // Resolve tags + const addedTags = await Promise.all( + tags.map(async (tag) => { + let productTag = await Tag.findOne({ name: tag }); + if (!productTag) { + productTag = await Tag.create({ name: tag }); // Ensure await here + } + return productTag._id; + }) + ); + + console.log(addedCategories); + console.log(addedTags); + + const product = await Product.create({ + name, + slug, + productType, + description, + shortDescription, + categories: addedCategories, + tags: addedTags, + productStatus, + publishDate, + attribute, + thumbnail, + gallery, + }); + + ApiResponse.success( + res, + "Product created successfully, Now add variations and prices for it", + product + ); + } catch (error) { + if (error.name === MONGOOSE_VALIDATION_ERROR) { + return ApiResponse.validationError( + res, + "Product validation failed", + invalidFieldMessage(error), + 400 + ); + } + if (error.code === MONGOOSE_DUPLICATE_KEY) { + return ApiResponse.conflict(res, "Product already exists", 400); + } + return ApiResponse.error(res, "Product creation failed", 500, error); + } +}); + +const getAllProducts = asyncHandler(async (req, res) => { + try { + const products = await Product.find(); + ApiResponse.success(res, "Products fetched successfully", products); + } catch (error) { + ApiResponse.error(res, "Error while fetching products", 500, error); + } +}); + +const getAProduct = asyncHandler(async (req, res) => { + try { + const product = await Product.findById(req.params.id); + ApiResponse.success(res, "Product fetched successfully", product); + } catch (error) { + if ( + error.name === MONGOOSE_CAST_ERROR && + error.kind === MONGOOSE_OBJECT_ID + ) { + return ApiResponse.notFound(res, "Invalid object id provided", 404); + } + + ApiResponse.error(res, "Error while fetching product", 500, error); + } +}); + +const updateAProduct = asyncHandler(async (req, res) => { + try { + const { id } = req.params; + const { categories, tags, attributes, ...updates } = req.body; // Default categories and tags to empty strings + + const allowedUpdates = [ + "name", + "slug", + "productType", + "description", + "shortDescription", + "productStatus", + "publishDate", + "thumbnail", + "gallery", + ]; + + // Filter only allowed fields + const fieldsToUpdate = Object.keys(updates).reduce((acc, key) => { + if (allowedUpdates.includes(key)) { + acc[key] = updates[key]; + } + return acc; + }, {}); + + // Check if product exists + const product = await Product.findById(id); + if (!product) { + return ApiResponse.notFound(res, "Product not found", 404); + } + + // Process categories and tags + const resolveItems = async (items, model) => { + return await Promise.all( + items.split(",").map(async (item) => { + const existingItem = await model.findOne({ name: item }); + return existingItem + ? existingItem._id + : (await model.create({ name: item }))._id; + }) + ); + }; + + if (categories) { + fieldsToUpdate.categories = await resolveItems(categories, Category); + } + if (tags) { + fieldsToUpdate.tags = await resolveItems(tags, Tag); + } + + // Handle attributes for SIMPLE products + if (fieldsToUpdate.productType === SIMPLE) { + fieldsToUpdate.attributes = attributes || null; + } + + // Update the product + const updatedProduct = await Product.findByIdAndUpdate( + id, + { $set: fieldsToUpdate }, + { new: true, runValidators: true } + ); + + ApiResponse.success(res, "Product updated successfully", updatedProduct); + } catch (error) { + if ( + error.name === MONGOOSE_CAST_ERROR && + error.kind === MONGOOSE_OBJECT_ID + ) { + return ApiResponse.notFound(res, "Invalid object id provided", 404); + } + if (error.name === MONGOOSE_VALIDATION_ERROR) { + return ApiResponse.validationError( + res, + "Product validation failed", + invalidFieldMessage(error), + 400 + ); + } + ApiResponse.error(res, "Error while updating product", 500, error); + } +}); + +module.exports = { + addAProduct, + getAllProducts, + getAProduct, + updateAProduct, +}; diff --git a/index.js b/index.js index b0316a9..fe462cc 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,7 @@ app.use("/api/v1/address", require("./routes/address.route")); app.use("/api/v1/category", require("./routes/category.route")); app.use("/api/v1/tag", require("./routes/tag.route")); app.use("/api/v1/attribute", require("./routes/attribute.route")); +app.use("/api/v1/product", require("./routes/product.routes")); app.all("*", (req, res) => { // res.redirect("/404.html") diff --git a/middleware/multer.middleware.js b/middleware/multer.middleware.js index bc0a6d4..a754983 100644 --- a/middleware/multer.middleware.js +++ b/middleware/multer.middleware.js @@ -7,7 +7,6 @@ const { validateUsername } = require("../utils/inputValidation/validators.js"); const storage = multer.diskStorage({ destination: function (req, _, cb) { const username = req.user?.username || validateUsername(req.body.username); - console.log(req.body); if (!username) { // Tell the multer to skip uploading the file @@ -28,7 +27,6 @@ const storage = multer.diskStorage({ }, filename: function (_, file, cb) { const uniqueName = uuidv4() + path.extname(file.originalname); - console.log(file.fieldname); cb(null, file.fieldname + "-" + uniqueName); }, }); diff --git a/model/category.model.js b/model/category.model.js index 69386b2..3147e91 100644 --- a/model/category.model.js +++ b/model/category.model.js @@ -1,5 +1,5 @@ const mongoose = require("mongoose"); -const slugify = require("slugify"); // For automatic slug generation +const slugify = require("slugify"); const { CATEGORY } = require("../constants/models.constants"); const { nameRegex } = require("../constants/regex.constants"); diff --git a/model/product.model.js b/model/product.model.js index c804b70..9acda77 100644 --- a/model/product.model.js +++ b/model/product.model.js @@ -1,4 +1,5 @@ const mongoose = require("mongoose"); +const slugify = require("slugify"); const { CATEGORY, TAG, @@ -10,6 +11,7 @@ const { SIMPLE, VARIABLE, } = require("../constants/models.constants"); +const { productNameRegex } = require("../constants/regex.constants"); const productSchema = new mongoose.Schema( { @@ -17,30 +19,33 @@ const productSchema = new mongoose.Schema( type: String, trim: true, unique: true, - required: true, + match: [productNameRegex, "Product Name not in proper format"], + required: [true, "Product Name is required"], }, slug: { type: String, trim: true, - required: true, - unique: true, + required: [true, "Product Slug is required"], + unique: [true, "Product slug must be unique"], }, productType: { type: String, enum: [SIMPLE, VARIABLE], - required: true, + default: SIMPLE, }, description: { type: String, - required: true, + required: [true, "Description is required"], trim: true, + minlength: [4, "Description must be at least 4 characters long"], + maxlength: [2048, "Description must be at most 2048 characters long"], }, shortDescription: { type: String, trim: true, - required: true, - minlength: 4, - maxlength: 512, + required: [true, "Short description is required"], + minlength: [4, "Short description must be at least 4 characters long"], + maxlength: [512, "Short description must be at most 512 characters long"], }, thumbnail: { type: String, diff --git a/routes/product.routes.js b/routes/product.routes.js new file mode 100644 index 0000000..91195fd --- /dev/null +++ b/routes/product.routes.js @@ -0,0 +1,34 @@ +const ROLES_LIST = require("../config/rolesList"); +const { + addAProduct, + getAllProducts, + getAProduct, + updateAProduct, +} = require("../controllers/product.controller"); +const upload = require("../middleware/multer.middleware"); +const verifyJWT = require("../middleware/verifyJWT.middleware"); +const verifyRoles = require("../middleware/verifyRoles.middleware"); + +const router = require("express").Router(); + +router + .route("/add-a-product") + .post( + verifyJWT, + verifyRoles(ROLES_LIST.ADMIN, ROLES_LIST.MANAGER), + upload.fields([{ name: "thumbnail", maxCount: 1 }, { name: "gallery" }]), + addAProduct + ); + +router.route("/get-all-products").get(getAllProducts); +router.route("/get-a-product/:id").get(getAProduct); + +router + .route("/update-a-product/:id") + .patch( + verifyJWT, + verifyRoles(ROLES_LIST.ADMIN, ROLES_LIST.MANAGER), + updateAProduct + ); + +module.exports = router;