diff --git a/backend/src/abstracts/product.abstract.ts b/backend/src/abstracts/product.abstract.ts new file mode 100644 index 0000000..3f29ad9 --- /dev/null +++ b/backend/src/abstracts/product.abstract.ts @@ -0,0 +1,34 @@ +type PrismaProduct = { + id: string; + title: string; + size: string; + color: string; + description: string; + gender: string; + category: string; + price: number; + imageUrl: string; + createdAt: Date; + updatedAt: Date; +}; + +type PrismaProducts = PrismaProduct[]; + +abstract class ProductProvider { + abstract getProducts(): Promise; + + abstract getProductById(id: string): Promise; + + abstract createProduct( + title: string, + size: string, + color: string, + description: string, + gender: string, + category: string, + price: number, + imageUrl: string + ): Promise; + + abstract deleteProduct(id: string): Promise; +} diff --git a/backend/src/controllers/algorithm.controller.ts b/backend/src/controllers/algorithm.controller.ts index cdeaff5..a1d09ba 100644 --- a/backend/src/controllers/algorithm.controller.ts +++ b/backend/src/controllers/algorithm.controller.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; -import { BadRequestError } from "../errors/errors.js"; +import { HttpBadRequestError } from "../errors/http.error.js"; import { AlgorithmServiceInterface } from "../../types.js"; class AlgorithmController { @@ -13,7 +13,7 @@ class AlgorithmController { try { const prompt: string = req.body.prompt; if (!prompt) { - return next(new BadRequestError("No string was provided!")); + return next(new HttpBadRequestError()); } const isEcoFriendly: boolean = @@ -23,8 +23,8 @@ class AlgorithmController { return res.status(200).json("Clothing is not eco-friendly!"); } return res.status(200).json("Clothing is eco-friendly!"); - } catch (err) { - next(err); + } catch (e) { + next(e); } }; } diff --git a/backend/src/controllers/product.controller.ts b/backend/src/controllers/product.controller.ts new file mode 100644 index 0000000..9af12be --- /dev/null +++ b/backend/src/controllers/product.controller.ts @@ -0,0 +1,70 @@ +import { Request, Response, NextFunction } from "express"; + +class ProductController { + constructor(private service: ProductProvider) { + this.service = service; + } + + getProducts = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise> | void> => { + try { + const products = await this.service.getProducts(); + return res.status(200).json(products); + } catch (e) { + next(e); + } + }; + + getProductById = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise> | void> => { + try { + const product = await this.service.getProductById(req.params.id); + return res.status(200).json(product); + } catch (e) { + next(e); + } + }; + + postProduct = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise> | void> => { + try { + const newProduct = await this.service.createProduct( + req.body.title, + req.body.size, + req.body.color, + req.body.description, + req.body.gender, + req.body.category, + req.body.price, + req.body.imageUrl + ); + return res.status(201).json(newProduct); + } catch (e) { + next(e); + } + }; + + deleteProduct = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise> | void> => { + try { + const product = await this.service.deleteProduct(req.params.id); + return res.status(200).json(product); + } catch (e) { + next(e); + } + }; +} + +export default ProductController; diff --git a/backend/src/errors/errors.ts b/backend/src/errors/errors.ts deleted file mode 100644 index 1450740..0000000 --- a/backend/src/errors/errors.ts +++ /dev/null @@ -1,27 +0,0 @@ -export class BadRequestError extends Error { - constructor(public message: string) { - super(message); - this.name = "BadRequestError"; - } -} - -export class UnauthorizedError extends Error { - constructor(public message: string) { - super(message); - this.name = "UnauthorizedError"; - } -} - -export class ForbiddenError extends Error { - constructor(public message: string) { - super(message); - this.name = "ForbiddenError"; - } -} - -export class NotFoundError extends Error { - constructor(public message: string) { - super(message); - this.name = "NotFoundError"; - } -} diff --git a/backend/src/errors/http.error.ts b/backend/src/errors/http.error.ts new file mode 100644 index 0000000..3b10d55 --- /dev/null +++ b/backend/src/errors/http.error.ts @@ -0,0 +1,7 @@ +export class HttpBadRequestError extends Error {} + +export class HttpUnauthorizedError extends Error {} + +export class HttpForbiddenError extends Error {} + +export class HttpNotFoundError extends Error {} diff --git a/backend/src/errors/prisma.error.ts b/backend/src/errors/prisma.error.ts new file mode 100644 index 0000000..7f1c0c5 --- /dev/null +++ b/backend/src/errors/prisma.error.ts @@ -0,0 +1,11 @@ +export class PrismaClientKnownRequestError extends Error {} + +export class PrismaClientUnknownRequestError extends Error {} + +export class PrismaClientRustPanicError extends Error {} + +export class PrismaClientInitializationError extends Error {} + +export class PrismaClientValidationError extends Error {} + +export class PrismaGenericError extends Error {} diff --git a/backend/src/errors/product.error.ts b/backend/src/errors/product.error.ts new file mode 100644 index 0000000..71f53fb --- /dev/null +++ b/backend/src/errors/product.error.ts @@ -0,0 +1 @@ +export class ProductNotFoundError extends Error {} diff --git a/backend/src/index.ts b/backend/src/index.ts index 5104c64..81ed20c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -2,12 +2,12 @@ import express, { Express } from "express"; import cors from "cors"; import { PORT } from "./config/config.js"; import errorHandler from "./middlewares/error.middleware.js"; - +import { PrismaClient } from "@prisma/client"; import algorithmRouter from "./routes/algorithm.routes.js"; +import productRouter from "./routes/product.routes.js"; -import { PrismaClient } from '@prisma/client' -const prisma = new PrismaClient() export const app: Express = express(); +export const prisma = new PrismaClient(); app.use(express.json()); @@ -15,31 +15,7 @@ app.use(cors()); app.use("/api/v1/algorithm", algorithmRouter); -app.post("/product", async (request, response) => { - const {title, size, color, description, gender, category, price, imageUrl} = request.body - const newProduct = await prisma.product.create({ - data: { - title, size, color, description, gender, category, price, imageUrl - } - }) - response.json(newProduct) -}) - -app.get("/products", async (request, response) => { - //add filtering - const prods = await prisma.product.findMany(); - - response.json(prods); -}); - -app.get("/product/:id", async (request, response) => { - let prodID = request.params.id; - const product = await prisma.product.findUnique({ - where: {id: prodID} - }) - - response.json(product); -}); +app.use("/api/v1/products", productRouter); app.use(errorHandler); diff --git a/backend/src/middlewares/error.middleware.ts b/backend/src/middlewares/error.middleware.ts index 38ddd85..becceca 100644 --- a/backend/src/middlewares/error.middleware.ts +++ b/backend/src/middlewares/error.middleware.ts @@ -1,30 +1,45 @@ import { Request, Response, NextFunction } from "express"; import { - BadRequestError, - ForbiddenError, - NotFoundError, - UnauthorizedError, -} from "../errors/errors.js"; - + HttpBadRequestError, + HttpForbiddenError, + HttpNotFoundError, + HttpUnauthorizedError, +} from "../errors/http.error.js"; +import { + PrismaClientInitializationError, + PrismaClientRustPanicError, + PrismaClientUnknownRequestError, + PrismaClientValidationError, + PrismaGenericError, +} from "../errors/prisma.error.js"; +import { ProductNotFoundError } from "../errors/product.error.js"; export default function errorHandler( - err: Error, + e: Error, req: Request, res: Response, next: NextFunction ) { - if (err instanceof BadRequestError) { - res.status(400).json(err.message); - } else if (err instanceof UnauthorizedError) { - res.status(401).json(err.message); - } else if (err instanceof ForbiddenError) { - res.status(403).json(err.message); - } else if (err instanceof NotFoundError) { - res.status(404).json(err.message); + if (e instanceof HttpBadRequestError) { + res.status(400).json({ error: "Bad Request Error" }); + } else if (e instanceof HttpUnauthorizedError) { + res.status(401).json({ error: "Unauthorized Error" }); + } else if (e instanceof HttpForbiddenError) { + res.status(403).json({ error: "Forbidden Error" }); + } else if (e instanceof HttpNotFoundError) { + res.status(404).json({ error: "Not Found Error" }); + } else if (e instanceof PrismaClientUnknownRequestError) { + res.status(500).json({ error: "Prisma Client Unknown Request Error" }); + } else if (e instanceof PrismaClientRustPanicError) { + res.status(500).json({ error: "Prisma Client Rust Panic Error" }); + } else if (e instanceof PrismaClientInitializationError) { + res.status(500).json({ error: "Prisma Client Initialization Error" }); + } else if (e instanceof PrismaClientValidationError) { + res.status(500).json({ error: "Prisma Client Validation Error" }); + } else if (e instanceof PrismaGenericError) { + res.status(500).json({ error: "Prisma Generic Error" }); + } else if (e instanceof ProductNotFoundError) { + res.status(500).json({ error: "Product Not Found Error" }); } else { - res.status(500).json({ - name: err.name, - message: err.message, - stack: err.stack, - }); + res.status(500).json({ error: "Unexpected error" }); } } diff --git a/backend/src/repositories/product.repository.ts b/backend/src/repositories/product.repository.ts new file mode 100644 index 0000000..d1e5e4c --- /dev/null +++ b/backend/src/repositories/product.repository.ts @@ -0,0 +1,130 @@ +import { prisma } from "../index.js"; +import { Prisma } from "@prisma/client"; +import { ProductNotFoundError } from "../errors/product.error.js"; +import { + PrismaClientInitializationError, + PrismaClientRustPanicError, + PrismaClientUnknownRequestError, + PrismaClientValidationError, + PrismaGenericError, +} from "../errors/prisma.error.js"; + +class ProductRepository implements ProductProvider { + getProducts = async (): Promise => { + try { + const products: PrismaProducts = await prisma.product.findMany(); + return products; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new PrismaGenericError(); + } else if (e instanceof Prisma.PrismaClientUnknownRequestError) { + throw new PrismaClientUnknownRequestError(); + } else if (e instanceof Prisma.PrismaClientRustPanicError) { + throw new PrismaClientRustPanicError(); + } else if (e instanceof Prisma.PrismaClientInitializationError) { + throw new PrismaClientInitializationError(); + } else if (e instanceof Prisma.PrismaClientValidationError) { + throw new PrismaClientValidationError(); + } else { + throw new PrismaGenericError(); + } + } + }; + + getProductById = async (id: string): Promise => { + try { + const product: PrismaProduct | null = await prisma.product.findUnique({ + where: { id: id }, + }); + if (product === null) { + throw new ProductNotFoundError(); + } + return product; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new PrismaGenericError(); + } else if (e instanceof Prisma.PrismaClientUnknownRequestError) { + throw new PrismaClientUnknownRequestError(); + } else if (e instanceof Prisma.PrismaClientRustPanicError) { + throw new PrismaClientRustPanicError(); + } else if (e instanceof Prisma.PrismaClientInitializationError) { + throw new PrismaClientInitializationError(); + } else if (e instanceof Prisma.PrismaClientValidationError) { + throw new PrismaClientValidationError(); + } else if (e instanceof ProductNotFoundError) { + throw new ProductNotFoundError(); + } else { + throw new PrismaGenericError(); + } + } + }; + + createProduct = async ( + title: string, + size: string, + color: string, + description: string, + gender: string, + category: string, + price: number, + imageUrl: string + ): Promise => { + try { + const newProduct: PrismaProduct = await prisma.product.create({ + data: { + title: title, + size: size, + color: color, + description: description, + gender: gender, + category: category, + price: price, + imageUrl: imageUrl, + }, + }); + return newProduct; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new PrismaGenericError(); + } else if (e instanceof Prisma.PrismaClientUnknownRequestError) { + throw new PrismaClientUnknownRequestError(); + } else if (e instanceof Prisma.PrismaClientRustPanicError) { + throw new PrismaClientRustPanicError(); + } else if (e instanceof Prisma.PrismaClientInitializationError) { + throw new PrismaClientInitializationError(); + } else if (e instanceof Prisma.PrismaClientValidationError) { + throw new PrismaClientValidationError(); + } else { + throw new PrismaGenericError(); + } + } + }; + + deleteProduct = async (id: string): Promise => { + try { + const product: PrismaProduct | null = await prisma.product.delete({ + where: { id: id }, + }); + if (product === null) { + throw new ProductNotFoundError(); + } + return product; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + throw new PrismaGenericError(); + } else if (e instanceof Prisma.PrismaClientUnknownRequestError) { + throw new PrismaClientUnknownRequestError(); + } else if (e instanceof Prisma.PrismaClientRustPanicError) { + throw new PrismaClientRustPanicError(); + } else if (e instanceof Prisma.PrismaClientInitializationError) { + throw new PrismaClientInitializationError(); + } else if (e instanceof Prisma.PrismaClientValidationError) { + throw new PrismaClientValidationError(); + } else { + throw new PrismaGenericError(); + } + } + }; +} + +export default ProductRepository; diff --git a/backend/src/routes/algorithm.routes.ts b/backend/src/routes/algorithm.routes.ts index 465e668..8a34326 100644 --- a/backend/src/routes/algorithm.routes.ts +++ b/backend/src/routes/algorithm.routes.ts @@ -7,7 +7,6 @@ import { } from "../../types.js"; const algorithmRouter: Router = Router(); - const algorithmService: AlgorithmServiceInterface = new AlgorithmService(); const algorithmController: AlgorithmControllerInterface = new AlgorithmController(algorithmService); diff --git a/backend/src/routes/product.routes.ts b/backend/src/routes/product.routes.ts new file mode 100644 index 0000000..d6eba30 --- /dev/null +++ b/backend/src/routes/product.routes.ts @@ -0,0 +1,16 @@ +import { Router } from "express"; +import ProductController from "../controllers/product.controller.js"; +import ProductService from "../services/product.service.js"; +import ProductRepository from "../repositories/product.repository.js"; + +const productRouter = Router(); +const productController = new ProductController( + new ProductService(new ProductRepository()) +); + +productRouter.get("/", productController.getProducts); +productRouter.get("/:id", productController.getProductById); +productRouter.post("/", productController.postProduct); +productRouter.delete("/:id", productController.deleteProduct); + +export default productRouter; diff --git a/backend/src/services/product.service.ts b/backend/src/services/product.service.ts new file mode 100644 index 0000000..7adba95 --- /dev/null +++ b/backend/src/services/product.service.ts @@ -0,0 +1,55 @@ +import { HttpBadRequestError } from "../errors/http.error.js"; + +class ProductService implements ProductProvider { + constructor(private provider: ProductProvider) { + this.provider = provider; + } + + getProducts = async (): Promise => { + return await this.provider.getProducts(); + }; + + getProductById = async (id: string): Promise => { + return await this.provider.getProductById(id); + }; + + createProduct = async ( + title: string, + size: string, + color: string, + description: string, + gender: string, + category: string, + price: number, + imageUrl: string + ): Promise => { + if ( + !title || + !size || + !color || + !description || + !gender || + !category || + !price || + !imageUrl + ) { + throw new HttpBadRequestError(); + } + return await this.provider.createProduct( + title, + size, + color, + description, + gender, + category, + price, + imageUrl + ); + }; + + deleteProduct = async (id: string): Promise => { + return await this.provider.deleteProduct(id); + }; +} + +export default ProductService;