Explorar el Código

Merge branch 'feature/products-table-feature' into develop

BedirhanOZCAN hace 1 mes
padre
commit
5cf8dc2586

+ 5 - 5
src/actions/auth/refreshToken/index.ts

@@ -1,18 +1,18 @@
 import jwt from "jsonwebtoken";
 import redis from "../../../config/redis";
 import {
-    User 
+    User
 } from "../../../models/User";
 import {
     RefreshTokenInput,
-    RefreshTokenResult 
+    RefreshTokenResult
 } from "./types";
 
 const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResult> => {
     const token = input.refreshToken!;
     try {
         const user = await User.findOne({
-            refreshToken: token 
+            refreshToken: token
         });
         if (!user) {
             return {
@@ -51,7 +51,7 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             },
             process.env.JWT_SECRET as string,
             {
-                expiresIn: "4h" 
+                expiresIn: "4h"
             }
         );
 
@@ -64,7 +64,7 @@ const refreshToken = async (input: RefreshTokenInput): Promise<RefreshTokenResul
             },
             process.env.JWT_SECRET as string,
             {
-                expiresIn: "30d" 
+                expiresIn: "30d"
             }
         );
 

+ 13 - 13
src/actions/auth/register/index.ts

@@ -1,9 +1,9 @@
 import {
-    User 
+    User
 } from "../../../models/User";
 import {
     RegisterInput,
-    RegisterResult 
+    RegisterResult
 } from "./types";
 
 const generateSlug = (companyName: string): string => {
@@ -25,29 +25,29 @@ const register = async (input: RegisterInput): Promise<RegisterResult> => {
     const {
         companyName,
         phoneNumber,
-        firstName, 
-        lastName, 
-        password, 
-        mail 
+        firstName,
+        lastName,
+        password,
+        mail
     } = input;
 
     const existingUser = await User.findOne({
-        mail 
+        mail
     });
     if (existingUser) {
         return {
-            message: "email-already-in-use", 
-            code: 409 
+            message: "email-already-in-use",
+            code: 409
         };
     }
 
     const existingPhone = await User.findOne({
-        phoneNumber 
+        phoneNumber
     });
     if (existingPhone) {
         return {
-            message: "phone-already-in-use", 
-            code: 409 
+            message: "phone-already-in-use",
+            code: 409
         };
     }
 
@@ -64,7 +64,7 @@ const register = async (input: RegisterInput): Promise<RegisterResult> => {
 
     return {
         message: "registration-successful",
-        code: 201 
+        code: 201
     };
 };
 

+ 51 - 0
src/actions/menu/addProduct/index.ts

@@ -0,0 +1,51 @@
+import {
+    Product
+} from "../../../models/Product";
+import {
+    AddProductResult
+} from "./types";
+
+const addProduct = async (body: any, context: { userID: string }): Promise<AddProductResult> => {
+    try {
+        const {
+            coverPhotoPath,
+            description,
+            isAvailable,
+            categoryID,
+            isActive,
+            title,
+            price,
+        } = body;
+
+        const {
+            userID
+        } = context;
+
+        const newProduct = await Product.create({
+            coverPhotoPath,
+            description,
+            isAvailable,
+            categoryID,
+            isActive,
+            title,
+            price,
+            userID: userID,
+        });
+
+        return {
+            message: "product-created-successfully",
+            code: 201,
+            payload: {
+                product: newProduct
+            }
+        };
+    } catch (error) {
+        console.error("AddProduct action error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default addProduct;

+ 46 - 0
src/actions/menu/addProduct/types.ts

@@ -0,0 +1,46 @@
+import {
+    IsNotEmpty,
+    IsOptional,
+    IsBoolean,
+    IsString,
+    IsNumber,
+    Min
+} from "class-validator";
+
+
+export class AddProductInput {
+    @IsString()
+    @IsNotEmpty({ message: "categoryID-is-required" })
+    categoryID!: string;
+
+    @IsString()
+    @IsNotEmpty({ message: "title-is-required" })
+    title!: string;
+
+    @IsString()
+    @IsOptional()
+    description?: string;
+
+    @Min(0, { message: "price-cannot-be-negative" })
+    @IsNumber()
+    @IsNotEmpty()
+    price!: number;
+
+    @IsString()
+    @IsOptional()
+    coverPhotoPath?: string;
+
+    @IsBoolean()
+    @IsOptional()
+    isActive?: boolean;
+
+    @IsBoolean()
+    @IsOptional()
+    isAvailable?: boolean;
+}
+
+export type AddProductResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 44 - 0
src/actions/menu/deleteProduct/index.ts

@@ -0,0 +1,44 @@
+import {
+    Product
+} from "../../../models/Product";
+import {
+    DeleteProductResult
+} from "./types";
+
+const deleteProduct = async (body: any, context: { userID: string }): Promise<DeleteProductResult> => {
+    try {
+        const {
+            productID
+        } = body;
+
+        const {
+            userID
+        } = context;
+
+        const deletedProduct = await Product.findOneAndDelete({
+            _id: productID,
+            userID: userID
+        });
+
+        if (!deletedProduct) {
+            return {
+                message: "product-not-found-or-unauthorized",
+                code: 404
+            };
+        }
+
+        return {
+            message: "product-deleted-successfully",
+            code: 200
+        };
+
+    } catch (error) {
+        console.error("DeleteProduct action error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default deleteProduct;

+ 16 - 0
src/actions/menu/deleteProduct/types.ts

@@ -0,0 +1,16 @@
+import {
+    IsNotEmpty,
+    IsString
+} from "class-validator";
+
+export class DeleteProductInput {
+    @IsString()
+    @IsNotEmpty({ message: "productID-is-required" })
+    productID!: string;
+}
+
+export type DeleteProductResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 77 - 0
src/actions/menu/getProducts/index.ts

@@ -0,0 +1,77 @@
+import mongoose from "mongoose";
+import {
+    Product
+} from "../../../models/Product";
+import {
+    GetProductsResult,
+    GetProductsInput
+} from "./types";
+
+const getProducts = async (query: GetProductsInput, context: { userID: string }): Promise<GetProductsResult> => {
+    try {
+        const {
+            userID
+        } = context;
+        const {
+            categoryID
+        } = query;
+
+        const matchStage: any = {
+            userID: new mongoose.Types.ObjectId(userID)
+        };
+
+        if (categoryID) {
+            matchStage.categoryID = new mongoose.Types.ObjectId(categoryID);
+        }
+
+        const products = await Product.aggregate([
+            {
+                $match: matchStage
+            },
+            {
+                $lookup: {
+                    from: "categories",
+                    localField: "categoryID",
+                    foreignField: "_id",
+                    as: "categoryID"
+                }
+            },
+            {
+                $unwind: {
+                    path: "$categoryID",
+                    preserveNullAndEmptyArrays: true
+                }
+            },
+            {
+                $addFields: {
+                    categoryID: {
+                        _id: "$categoryID._id",
+                        title: "$categoryID.title",
+                        isActive: "$categoryID.isActive"
+                    }
+                }
+            },
+            {
+                $sort: {
+                    createdAt: -1
+                }
+            }
+        ]);
+
+        return {
+            message: "products-retrieved-successfully",
+            code: 200,
+            payload: {
+                products
+            }
+        };
+    } catch (error) {
+        console.error("GetProducts action error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default getProducts;

+ 16 - 0
src/actions/menu/getProducts/types.ts

@@ -0,0 +1,16 @@
+import {
+    IsOptional,
+    IsString
+} from "class-validator";
+
+export class GetProductsInput {
+    @IsString()
+    @IsOptional()
+    categoryID?: string;
+}
+
+export type GetProductsResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 18 - 5
src/actions/menu/index.ts

@@ -1,12 +1,25 @@
 export {
-    default as addCategory 
+    default as addCategory
 } from "./addCategory";
 export {
-    default as deleteCategory 
+    default as deleteCategory
 } from "./deleteCategory";
 export {
-    default as getCategories 
+    default as getCategories
 } from "./getCategories";
 export {
-    default as updateCategory 
-} from "./updateCategory";
+    default as updateCategory
+} from "./updateCategory";
+
+export {
+    default as addProduct
+} from "./addProduct";
+export {
+    default as getProducts
+} from "./getProducts";
+export {
+    default as updateProduct
+} from "./updateProduct";
+export {
+    default as deleteProduct
+} from "./deleteProduct";

+ 12 - 2
src/actions/menu/types/index.ts

@@ -1,9 +1,19 @@
 export {
-    AddCategoryInput 
+    AddCategoryInput
 } from "../addCategory/types";
 export {
     DeleteCategoryInput
 } from "../deleteCategory/types";
 export {
     UpdateCategoryInput
-} from "../updateCategory/types";
+} from "../updateCategory/types";
+
+export {
+    AddProductInput
+} from "../addProduct/types";
+export {
+    UpdateProductInput
+} from "../updateProduct/types";
+export {
+    DeleteProductInput
+} from "../deleteProduct/types";

+ 106 - 0
src/actions/menu/updateProduct/index.ts

@@ -0,0 +1,106 @@
+import mongoose from "mongoose";
+import {
+    Product
+} from "../../../models/Product";
+import {
+    UpdateProductResult
+} from "./types";
+
+const updateProduct = async (body: any, context: { userID: string }): Promise<UpdateProductResult> => {
+    try {
+        const {
+            coverPhotoPath,
+            description,
+            isAvailable,
+            categoryID,
+            productID,
+            isActive,
+            price,
+            title,
+        } = body;
+
+        const {
+            userID
+        } = context;
+
+        const updateData: any = {
+        };
+
+        if (categoryID !== undefined) updateData.categoryID = new mongoose.Types.ObjectId(categoryID);
+
+        if (title !== undefined) updateData.title = title;
+        if (description !== undefined) updateData.description = description;
+        if (price !== undefined) updateData.price = price;
+        if (coverPhotoPath !== undefined) updateData.coverPhotoPath = coverPhotoPath;
+        if (isActive !== undefined) updateData.isActive = isActive;
+        if (isAvailable !== undefined) updateData.isAvailable = isAvailable;
+
+        const updatedDoc = await Product.findOneAndUpdate(
+            {
+                _id: new mongoose.Types.ObjectId(productID),
+                userID: new mongoose.Types.ObjectId(userID)
+            },
+            {
+                $set: updateData
+            },
+            {
+                new: true
+            }
+        );
+
+        if (!updatedDoc) {
+            return {
+                message: "product-not-found-or-unauthorized",
+                code: 404
+            };
+        }
+
+        const aggregatedProduct = await Product.aggregate([
+            {
+                $match: {
+                    _id: new mongoose.Types.ObjectId(productID)
+                }
+            },
+            {
+                $lookup: {
+                    from: "categories",
+                    localField: "categoryID",
+                    foreignField: "_id",
+                    as: "categoryID"
+                }
+            },
+            {
+                $unwind: {
+                    path: "$categoryID",
+                    preserveNullAndEmptyArrays: true
+                }
+            },
+            {
+                $addFields: {
+                    categoryID: {
+                        _id: "$categoryID._id",
+                        title: "$categoryID.title",
+                        isActive: "$categoryID.isActive"
+                    }
+                }
+            }
+        ]);
+
+        return {
+            message: "product-updated-successfully",
+            code: 200,
+            payload: {
+                product: aggregatedProduct[0]
+            }
+        };
+
+    } catch (error) {
+        console.error("UpdateProduct action error:", error);
+        return {
+            message: "internal-server-error",
+            code: 500
+        };
+    }
+};
+
+export default updateProduct;

+ 50 - 0
src/actions/menu/updateProduct/types.ts

@@ -0,0 +1,50 @@
+import {
+    IsNotEmpty,
+    IsOptional,
+    IsBoolean,
+    IsString,
+    IsNumber,
+    Min
+} from "class-validator";
+
+export class UpdateProductInput {
+    @IsString()
+    @IsNotEmpty({ message: "productID-is-required" })
+    productID!: string;
+
+    @IsString()
+    @IsOptional()
+    categoryID?: string;
+
+    @IsString()
+    @IsOptional()
+    title?: string;
+
+    @IsString()
+    @IsOptional()
+    description?: string;
+
+    @Min(0, { message: "price-cannot-be-negative" })
+    @IsNumber()
+    @IsOptional()
+    price?: number;
+
+    @IsString()
+    @IsOptional()
+    coverPhotoPath?: string;
+
+    @IsBoolean()
+    @IsOptional()
+    isActive?: boolean;
+
+    @IsBoolean()
+    @IsOptional()
+    isAvailable?: boolean;
+
+}
+
+export type UpdateProductResult = {
+    message: string;
+    code: number;
+    payload?: any;
+};

+ 67 - 11
src/controllers/menuController.ts

@@ -1,14 +1,18 @@
 import {
-    Response 
+    Response
 } from "express";
 import {
-    AuthRequest 
+    AuthRequest
 } from "../middlewares/authMiddleware";
 import {
     deleteCategory as _deleteCategory,
     updateCategory as _updateCategory,
+    deleteProduct as _deleteProduct,
+    updateProduct as _updateProduct,
     getCategories as _getCategories,
-    addCategory as _addCategory
+    getProducts as _getProducts,
+    addCategory as _addCategory,
+    addProduct as _addProduct
 } from "../actions/menu";
 
 export const addCategory = async (req: AuthRequest, res: Response): Promise<void> => {
@@ -16,8 +20,8 @@ export const addCategory = async (req: AuthRequest, res: Response): Promise<void
 
     res.status(result.code)
         .json({
-            message: result.message, 
-            code: result.code 
+            message: result.message,
+            code: result.code
         });
 };
 
@@ -26,18 +30,18 @@ export const deleteCategory = async (req: AuthRequest, res: Response): Promise<v
 
     res.status(result.code)
         .json({
-            message: result.message, 
-            code: result.code 
+            message: result.message,
+            code: result.code
         });
 };
 
 export const updateCategory = async (req: AuthRequest, res: Response): Promise<void> => {
     const result = await _updateCategory(req.context!.userID, req.body);
-    
+
     res.status(result.code)
         .json({
-            message: result.message, 
-            code: result.code 
+            message: result.message,
+            code: result.code
         });
 };
 
@@ -49,7 +53,59 @@ export const getCategories = async (req: AuthRequest, res: Response): Promise<vo
             message: result.message,
             code: result.code,
             ...(result.payload && {
-                payload: result.payload 
+                payload: result.payload
             }),
         });
+};
+
+export const addProduct = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _addProduct(req.body, req.context!);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+
+export const getProducts = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _getProducts(req.query, req.context!);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+
+export const updateProduct = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _updateProduct(req.body, req.context!);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
+};
+
+export const deleteProduct = async (req: AuthRequest, res: Response): Promise<void> => {
+    const result = await _deleteProduct(req.body, req.context!);
+
+    res.status(result.code)
+        .json({
+            message: result.message,
+            code: result.code,
+            ...(result.payload && {
+                payload: result.payload
+            })
+        });
 };

+ 10 - 10
src/middlewares/authMiddleware.ts

@@ -31,24 +31,24 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
             return;
         }
 
-        let decoded: { 
+        let decoded: {
             companyName: string;
             fullName: string;
             userID: string;
-            token:string;
+            token: string;
         };
-        
+
         try {
-            decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { 
+            decoded = jwt.verify(token, process.env.JWT_SECRET as string) as {
                 companyName: string;
                 fullName: string;
                 userID: string,
-                token:string;
+                token: string;
             };
         } catch (err) {
             res.status(401)
                 .json({
-                    message: "expired-token", 
+                    message: "expired-token",
                     code: 401
                 });
             return;
@@ -57,7 +57,7 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
         if (!decoded || !decoded.userID) {
             res.status(401)
                 .json({
-                    message: "invalid-token", 
+                    message: "invalid-token",
                     code: 401
                 });
             return;
@@ -67,7 +67,7 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
         if (!cachedToken) {
             res.status(401)
                 .json({
-                    message: "expired-token", 
+                    message: "expired-token",
                     code: 401
                 });
             return;
@@ -86,7 +86,7 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
         if (!user) {
             res.status(401)
                 .json({
-                    message: "user-not-found", 
+                    message: "user-not-found",
                     code: 401
                 });
             return;
@@ -103,7 +103,7 @@ export const authMiddleware = async (req: AuthRequest, res: Response, next: Next
     } catch (error) {
         res.status(401)
             .json({
-                message: "invalid-token", 
+                message: "invalid-token",
                 code: 401
             });
     }

+ 11 - 7
src/middlewares/validateBody.ts

@@ -1,27 +1,31 @@
 import {
-    plainToInstance 
+    plainToInstance
 } from "class-transformer";
 import {
-    validate 
+    validate
 } from "class-validator";
 import {
-    Request,
+    NextFunction,
     Response,
-    NextFunction 
+    Request
 } from "express";
 import {
-    formatValidationErrors 
+    formatValidationErrors
 } from "../utils";
 
 export const validateBody = (dto: any) => {
     return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
         const instance = plainToInstance(dto, req.body);
-        const errors = await validate(instance);
+
+        const errors = await validate(instance, {
+            whitelist: true,
+            forbidNonWhitelisted: false
+        });
 
         if (errors.length > 0) {
             res.status(400).json({
                 message: formatValidationErrors(errors),
-                code: 400 
+                code: 400
             });
             return;
         }

+ 60 - 0
src/models/Product.ts

@@ -0,0 +1,60 @@
+import mongoose, {
+    Document,
+    Schema
+} from "mongoose";
+
+export interface IProduct extends Document {
+    categoryID: mongoose.Types.ObjectId;
+    userID: mongoose.Types.ObjectId;
+    title: string;
+    description: string;
+    price: number;
+    coverPhotoPath: string;
+    isActive: boolean;
+    isAvailable: boolean;
+}
+
+const productSchema = new Schema<IProduct>(
+    {
+        categoryID: {
+            type: mongoose.Schema.Types.ObjectId,
+            index: true
+        },
+        userID: {
+            type: mongoose.Schema.Types.ObjectId,
+            index: true
+        },
+        title: {
+            type: String,
+            required: true,
+            trim: true
+        },
+        description: {
+            type: String,
+            default: "",
+            trim: true
+        },
+        price: {
+            type: Number,
+            required: true,
+            min: 0
+        },
+        coverPhotoPath: {
+            type: String,
+            default: ""
+        },
+        isActive: {
+            type: Boolean,
+            default: true
+        },
+        isAvailable: {
+            type: Boolean,
+            default: true
+        },
+    },
+    {
+        timestamps: true,
+    }
+);
+
+export const Product = mongoose.models.Product || mongoose.model<IProduct>("Product", productSchema);

+ 1 - 1
src/routes/index.ts

@@ -1,5 +1,5 @@
 import {
-    Router 
+    Router
 } from "express";
 import authRoutes from "./authRoutes";
 import menuRoutes from "./menuRoutes";

+ 21 - 9
src/routes/menuRoutes.ts

@@ -1,23 +1,30 @@
 import {
-    Router 
+    Router
 } from "express";
 import {
     deleteCategory,
     updateCategory,
     getCategories,
-    addCategory
+    deleteProduct,
+    updateProduct,
+    addCategory,
+    getProducts,
+    addProduct
 } from "../controllers/menuController";
 import {
-    authMiddleware 
+    UpdateCategoryInput,
+    DeleteCategoryInput,
+    UpdateProductInput,
+    DeleteProductInput,
+    AddCategoryInput,
+    AddProductInput
+} from "../actions/menu/types";
+import {
+    authMiddleware
 } from "../middlewares/authMiddleware";
 import {
-    validateBody 
+    validateBody
 } from "../middlewares/validateBody";
-import {
-    AddCategoryInput,
-    DeleteCategoryInput,
-    UpdateCategoryInput
-} from "../actions/menu/types";
 
 const router = Router();
 
@@ -26,4 +33,9 @@ router.put("/updateCategory", authMiddleware, validateBody(UpdateCategoryInput),
 router.post("/addCategory", authMiddleware, validateBody(AddCategoryInput), addCategory);
 router.get("/getCategories", authMiddleware, getCategories);
 
+router.delete("/deleteProduct", authMiddleware, validateBody(DeleteProductInput), deleteProduct);
+router.put("/updateProduct", authMiddleware, validateBody(UpdateProductInput), updateProduct);
+router.post("/addProduct", authMiddleware, validateBody(AddProductInput), addProduct);
+router.get("/getProducts", authMiddleware, getProducts);
+
 export default router;