Skip to content

Commit

Permalink
Add Product endpoints and controller for simple products
Browse files Browse the repository at this point in the history
  • Loading branch information
bewake24 committed Nov 18, 2024
1 parent 1c01b7e commit d17a10a
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 13 deletions.
4 changes: 2 additions & 2 deletions constants/models.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions constants/regex.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,4 +28,5 @@ module.exports = {
addressRegex,
cityRegex,
pincodeRegex,
productNameRegex,
};
209 changes: 209 additions & 0 deletions controllers/product.controller.js
Original file line number Diff line number Diff line change
@@ -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,
};
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 0 additions & 2 deletions middleware/multer.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
},
});
Expand Down
2 changes: 1 addition & 1 deletion model/category.model.js
Original file line number Diff line number Diff line change
@@ -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");

Expand Down
21 changes: 13 additions & 8 deletions model/product.model.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const mongoose = require("mongoose");
const slugify = require("slugify");
const {
CATEGORY,
TAG,
Expand All @@ -10,37 +11,41 @@ const {
SIMPLE,
VARIABLE,
} = require("../constants/models.constants");
const { productNameRegex } = require("../constants/regex.constants");

const productSchema = new mongoose.Schema(
{
name: {
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,
Expand Down
34 changes: 34 additions & 0 deletions routes/product.routes.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit d17a10a

Please sign in to comment.