diff --git a/backend/.github/workflows/ci.yml b/.github/workflows/ci.yml similarity index 57% rename from backend/.github/workflows/ci.yml rename to .github/workflows/ci.yml index 433c1cb..fdf910f 100644 --- a/backend/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,17 +5,17 @@ on: branches: - main - dev - -jobs: - testing: +jobs: + build: runs-on: ubuntu-latest + steps: - name: Checkout - uses: actions/checkout@v2 - + uses: actions/checkout@v3 + - name: Install dependencies - run: npm install + run: cd backend && npm install - - name: Test - run: npm run test + - name: Build + run: cd backend && npm run build diff --git a/.gitignore b/.gitignore index 1c89893..75b489a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules .env dist migrations -.vscode +.DS_Store +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbb7699 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 techstartucalgary + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cbd01f1..189c565 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # โ™ป๏ธ ReThread +[![Continuous Integration](https://github.com/techstartucalgary/fashion/actions/workflows/ci.yml/badge.svg)](https://github.com/techstartucalgary/fashion/actions/workflows/ci.yml) +![GitHub repo size](https://img.shields.io/github/repo-size/techstartucalgary/rethread?logo=github&color=blue) +![Github tag](https://img.shields.io/github/v/tag/techstartucalgary/rethread?logo=github&color=red) +![GitHub contributors](https://img.shields.io/github/contributors/techstartucalgary/rethread?logo=github&color=yellow) +![Github pull requests](https://img.shields.io/github/issues-pr/techstartucalgary/rethread?logo=github) +![Github license](https://img.shields.io/github/license/techstartucalgary/rethread?logo=github&color=orange) + ## ๐Ÿ“– Table of Contents - [๐Ÿ“ Contributors](#-contributors) -- [๐Ÿ‘จโ€๐Ÿ’ป Teck Stack](#-tech-stack) +- [๐Ÿ‘จโ€๐Ÿ’ป Tech Stack](#-tech-stack) - [๐Ÿš€ Backend Documentation](#-backend-documentation) - - [๐Ÿƒ Quickstart](#-quickstart) + - [๐Ÿƒ Quick start](#-quick-start) - [๐Ÿ› ๏ธ Installation](#๏ธ-installation) - [๐Ÿงช Testing](#-testing) - [๐Ÿšง Development Environment](#-development-environment) @@ -34,38 +41,27 @@ ![Swift](https://img.shields.io/badge/Swift-F05138.svg?style=for-the-badge&logo=Swift&logoColor=white) ![SwiftUI](https://img.shields.io/badge/SwiftUI-2d68f3.svg?style=for-the-badge&logo=Swift&logoColor=black) - ![Xcode](https://img.shields.io/badge/Xcode-1575F9.svg?style=for-the-badge&logo=Xcode&logoColor=white) ![Figma](https://img.shields.io/badge/Figma-F24E1E.svg?style=for-the-badge&logo=Figma&logoColor=white) - - - Testing - - ![XCTest](https://img.shields.io/badge/XCTest-6ACD4D.svg?style=for-the-badge&logo=Apple&logoColor=white) + ![Xcode](https://img.shields.io/badge/Xcode-1575F9.svg?style=for-the-badge&logo=Xcode&logoColor=white) - Backend ![Typescript](https://img.shields.io/badge/TypeScript-3178C6.svg?style=for-the-badge&logo=TypeScript&logoColor=white) - ![Node.js](https://img.shields.io/badge/Node.js-339933.svg?style=for-the-badge&logo=nodedotjs&logoColor=white) ![Express](https://img.shields.io/badge/Express-000000.svg?style=for-the-badge&logo=Express&logoColor=white) - ![OpenAI](https://img.shields.io/badge/OpenAI-412991.svg?style=for-the-badge&logo=OpenAI&logoColor=white) - ![Prisma](https://img.shields.io/badge/Prisma-2D3748.svg?style=for-the-badge&logo=Prisma&logoColor=white) - ![MySQL](https://img.shields.io/badge/MySQL-4479A1.svg?style=for-the-badge&logo=MySQL&logoColor=white) - ![Redis](https://img.shields.io/badge/Redis-DC382D.svg?style=for-the-badge&logo=Redis&logoColor=white) - - - Testing - - ![Mocha](https://img.shields.io/badge/Mocha-8D6748.svg?style=for-the-badge&logo=Mocha&logoColor=white) - ![Chai](https://img.shields.io/badge/Chai-A30701.svg?style=for-the-badge&logo=Chai&logoColor=white) + ![Node.js](https://img.shields.io/badge/Node.js-339933.svg?style=for-the-badge&logo=nodedotjs&logoColor=white) + ![Prisma](https://img.shields.io/badge/Prisma-5a67d8.svg?style=for-the-badge&logo=Prisma&logoColor=white) + ![MySQL](https://img.shields.io/badge/MySQL-3e6e93.svg?style=for-the-badge&logo=MySQL&logoColor=white) -- CI/CD +- Cloud - ![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF.svg?style=for-the-badge&logo=GitHub%20Actions&logoColor=white) + ![PlanetScale](https://img.shields.io/badge/PlanetScale-000000.svg?style=for-the-badge&logo=PlanetScale&logoColor=white) ## ๐Ÿš€ Backend Documentation All the code is located in the `backend/src` directory. The backend is written using [Node.js](https://nodejs.org/en/) and [Express](https://expressjs.com/). -### ๐Ÿƒ Quickstart +### ๐Ÿƒ Quick start 1. Open the terminal and clone this repository using HTTPS or SSH (The example below uses SSH). @@ -85,18 +81,6 @@ cd fashion cd backend ``` -4. Run `npm install --only=production` to install all the dependencies. - -```bash -npm install -``` - -5. Run `npm run start` to start the server. - -```bash -npm run start -``` - ### ๐Ÿ› ๏ธ Installation 1. Make sure you have `Node.js` and `NPM` installed on your machine. Click [here](https://nodejs.org/en/) to download and install Node.js. Make sure you install the LTS version. NPM is installed automatically when you install Node.js. @@ -115,25 +99,17 @@ npm -v 4. If you see the version number of `Node.js` and `npm` then you are good to go. If not, then try to reinstall `Node.js`. -5. Make sure you have `MySQL` installed on your machine. Click [here](https://dev.mysql.com/downloads/mysql/) to download and install MySQL. Make sure you install the latest version. +5. Make sure you have `Docker Desktop` installed on your machine. Click [here](https://www.docker.com/products/docker-desktop) to download and install Docker Desktop. Make sure you install the latest version. -6. Open the terminal and run `mysql --version` to check if `MySQL` is installed. +6. Make sure you have `Git` installed on your machine. Click [here](https://git-scm.com/downloads) to download and install Git. Make sure you install the latest version. -```bash -mysql --version -``` - -7. If you see the version number of `MySQL` then you are good to go. If not, then try to reinstall `MySQL`. - -8. Make sure you have `Redis` installed on your machine. Click [here](https://redis.io/download) to download and install Redis. Make sure you install the latest version. - -9. Open the terminal and run `redis-server --version` to check if `Redis` is installed. +7. Open the terminal and run `git -v` to check if `Git` is installed. ```bash -redis-server --version +git -v ``` -10. If you see the version number of `Redis` then you are good to go. If not, then try to reinstall `Redis`. +8. If you see the version number of `Git` then you are good to go. If not, then try to reinstall `Git`. ### ๐Ÿงช Testing @@ -189,16 +165,26 @@ cd backend npm install ``` -5. Run `npm run start` to start the server. +5. Run `npx prisma init` to initialize the database. ```bash -npm run dev +npx prisma init +``` + +6. Update your `prisma/schema.prisma` file within the `backend` folder to use the `mysql` provider and set the relation mode type to `prisma`. + +```prisma +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + relationMode = "prisma" +} ``` -6. Run `npx prisma studio` to open Prisma Studio and view the database schema (Optional). +7. Once you are ready to push your schema to PlanetScale, run `prisma db push` against your PlanetScale database to update the schema in your database. ```bash -npx prisma studio +npx prisma db push ``` diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..11ddd8d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,3 @@ +node_modules +# Keep environment variables out of version control +.env diff --git a/backend/package.json b/backend/package.json index 3643095..61e1f14 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,12 +6,9 @@ "type": "module", "scripts": { "build": "npx tsc", - "prestart": "npx prisma migrate deploy && npx prisma generate", - "start": "NODE_ENV=PROD node dist/src/index.js", - "predev": "npx prisma migrate dev --name init && npx prisma generate", - "dev": "NODE_ENV=DEV nodemon -e ts --exec \"npm run build && node dist/src/index.js\"", - "pretest": "npx prisma migrate deploy && npx prisma generate", - "test": "NODE_ENV=TEST npm run build && mocha \"dist/test/**/*.test.js\"" + "start": "node dist/src/index.js", + "dev": "nodemon -e ts --exec \"npm run build && node dist/src/index.js\"", + "test": "npm run build && mocha \"dist/test/**/*.test.js\"" }, "keywords": [], "author": "", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 2a2f383..808daa4 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -3,16 +3,43 @@ generator client { } datasource db { - provider = "mysql" - url = env("DATABASE_URL") + provider = "mysql" + url = env("DATABASE_URL") + relationMode = "prisma" } -model User { +model Product { + id String @id @default(uuid()) + title String @unique + size String + color String + description String + gender String + category String + price Float + imageUrl String @map("image_url") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") +} + +model Order { id String @id @default(uuid()) - email String @unique - password String - firstName String - lastName String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + email String + productId Int @map("product_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") +} + +model User { + id String @id @default(uuid()) + phoneNumber String @unique @map("phone_number") + email String @unique + password String + firstName String @map("first_name") + lastName String @map("last_name") + gender String + postalCode String @map("postal_code") + dateOfBirth DateTime @map("date_of_birth") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") } 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/authentication.controller.ts b/backend/src/controllers/authentication.controller.ts deleted file mode 100644 index e8aaf8c..0000000 --- a/backend/src/controllers/authentication.controller.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { - AuthenticationControllerInterface, - AuthenticationServiceInterface, -} from "../../types.js"; - -class AuthenticationController implements AuthenticationControllerInterface { - constructor(private authentication: AuthenticationServiceInterface) {} - - postLogin = async ( - req: Request, - res: Response, - next: NextFunction - ): Promise>> => { - // TODO: Implement postLogin controller (Anfaal) - }; - - postSignup = async ( - req: Request, - res: Response, - next: NextFunction - ): Promise>> => { - // TODO: Implement postSignup controller (Ryan) - }; - - postLogout = async ( - req: Request, - res: Response, - next: NextFunction - ): Promise>> => { - // TODO: Implement postLogout controller (Alison) - }; -} - -export default AuthenticationController; 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 6e28f84..81ed20c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,23 +1,22 @@ import express, { Express } from "express"; import cors from "cors"; -import algorithmRouter from "./routers/algorithm.router.js"; -import authenticationRouter from "./routers/authentication.router.js"; -import errorHandler from "./middlewares/error.middleware.js"; 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"; export const app: Express = express(); - export const prisma = new PrismaClient(); -app.use(cors()); - app.use(express.json()); -app.use("/auth", authenticationRouter); +app.use(cors()); app.use("/api/v1/algorithm", algorithmRouter); +app.use("/api/v1/products", productRouter); + app.use(errorHandler); app.listen(PORT, () => { 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/models/user.model.ts b/backend/src/models/user.model.ts deleted file mode 100644 index 0a3372e..0000000 --- a/backend/src/models/user.model.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { UserInterface } from "../../types.js"; - -class User implements UserInterface { - constructor( - private email: string, - private password: string, - private firstName: string, - private lastName: string - ) {} - - getEmail(): string { - return this.email; - } - - getPassword(): string { - return this.password; - } - - getFirstName(): string { - return this.firstName; - } - - getLastName(): string { - return this.lastName; - } - - setEmail(email: string): void { - this.email = email; - } - - setPassword(password: string): void { - this.password = password; - } - - setFirstName(firstName: string): void { - this.firstName = firstName; - } - - setLastName(lastName: string): void { - this.lastName = lastName; - } -} 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/repositories/user.repository.ts b/backend/src/repositories/user.repository.ts deleted file mode 100644 index f1cb1e8..0000000 --- a/backend/src/repositories/user.repository.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { - PrismaUserInterface, - UserInterface, - UserRepositoryInterface, -} from "../../types.js"; - -class UserRepository implements UserRepositoryInterface { - constructor(private prisma: PrismaClient) {} - - async createUser(user: UserInterface): Promise { - const newUser = await this.prisma.user.create({ - data: { - email: user.getEmail(), - password: user.getPassword(), - firstName: user.getFirstName(), - lastName: user.getLastName(), - }, - }); - return newUser; - } - - async getUserByEmail(email: string): Promise { - const user = await this.prisma.user.findUnique({ - where: { - email: email, - }, - }); - - return user; - } -} - -export default UserRepository; diff --git a/backend/src/routers/authentication.router.ts b/backend/src/routers/authentication.router.ts deleted file mode 100644 index 3d7f66d..0000000 --- a/backend/src/routers/authentication.router.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Router } from "express"; -import { prisma } from "../index.js"; -import UserRepository from "../repositories/user.repository.js"; -import AuthenticationService from "../services/authentication.service.js"; -import AuthenticationController from "../controllers/authentication.controller.js"; -import { - AuthenticationControllerInterface, - AuthenticationServiceInterface, - UserRepositoryInterface, -} from "../../types.js"; - -const authenticationRouter = Router(); - -const userRepository: UserRepositoryInterface = new UserRepository(prisma); - -const authenticationService: AuthenticationServiceInterface = - new AuthenticationService(userRepository); - -const authenticationController: AuthenticationControllerInterface = - new AuthenticationController(authenticationService); - -authenticationRouter.post("/login", authenticationController.postLogin); - -authenticationRouter.post("/signup", authenticationController.postSignup); - -authenticationRouter.post("/logout", authenticationController.postLogout); - -export default authenticationRouter; diff --git a/backend/src/routers/algorithm.router.ts b/backend/src/routes/algorithm.routes.ts similarity index 99% rename from backend/src/routers/algorithm.router.ts rename to backend/src/routes/algorithm.routes.ts index 465e668..8a34326 100644 --- a/backend/src/routers/algorithm.router.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/authentication.service.ts b/backend/src/services/authentication.service.ts deleted file mode 100644 index 0fba607..0000000 --- a/backend/src/services/authentication.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { UserRepositoryInterface } from "../../types"; -import UserRepository from "../repositories/user.repository"; - -class AuthenticationService implements AuthenticationService { - constructor(private UserRepository: UserRepositoryInterface) {} - - async login(): Promise { - // TODO: Implement login and initialize session (Anfaal) - } - - async signup(): Promise { - // TODO: Implement signup and initialize session (Ryan) - } - - async logout(): Promise { - // TODO: Implement logout and destroy session (Alison) - } -} - -export default AuthenticationService; 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; diff --git a/backend/test/algorithm.test.ts b/backend/test/algorithm.test.ts index 3b5b72c..be97a91 100644 --- a/backend/test/algorithm.test.ts +++ b/backend/test/algorithm.test.ts @@ -12,25 +12,49 @@ const testString3: String = "100% cotton Machine wash cold Tumble dry low Not for use as pants"; describe("Algorithm", () => { + it("should return eco-friendly clothing", async () => { + const res: request.Response = await http + .post("/api/v1/algorithm") + .send({ prompt: testString3 }); + assert.equal(res.status, 200); + assert.equal( + res.body, + "Clothing is eco-friendly!", + "Body should be 'Clothing is eco-friendly!'" + ); + }); + it("should return eco-friendly clothing", async () => { const res: request.Response = await http .post("/api/v1/algorithm") .send({ prompt: testString2 }); assert.equal(res.status, 200); - assert.equal(res.body, "Clothing is eco-friendly!"); + assert.equal( + res.body, + "Clothing is eco-friendly!", + "Body should be 'Clothing is eco-friendly!'" + ); }); it("should return not eco-friendly clothing", async () => { const res: request.Response = await http .post("/api/v1/algorithm") .send({ prompt: testString1 }); - assert.equal(res.status, 200); - assert.equal(res.body, "Clothing is not eco-friendly!"); + assert.equal(res.status, 200, "Status code should be 200"); + assert.equal( + res.body, + "Clothing is not eco-friendly!", + "Body should be 'Clothing is not eco-friendly!'" + ); }); it("should return a BadRequestError", async () => { const res: request.Response = await http.post("/api/v1/algorithm").send({}); assert.equal(res.status, 400); - assert.equal(res.body, "No string was provided!"); + assert.equal( + res.body, + "No string was provided!", + "Body should be 'No string was provided!'" + ); }); }); diff --git a/backend/test/authentication.test.ts b/backend/test/authentication.test.ts deleted file mode 100644 index 16356d3..0000000 --- a/backend/test/authentication.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import request from "supertest"; -import { assert } from "chai"; -import { http } from "./config.test.js"; - -describe("Authentication", () => { - // TODO: Add tests for authentication and session management -}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index ab5f80a..ca771a3 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ diff --git a/backend/types.ts b/backend/types.ts index 9483305..474bcc1 100644 --- a/backend/types.ts +++ b/backend/types.ts @@ -26,53 +26,3 @@ export interface AlgorithmControllerInterface { export interface AlgorithmServiceInterface { isClothingEcoFriendly(prompt: string): Promise; } - -export interface AuthenticationControllerInterface { - postLogin: ( - req: Request, - res: Response, - next: NextFunction - ) => Promise>>; - postSignup: ( - req: Request, - res: Response, - next: NextFunction - ) => Promise>>; - postLogout: ( - req: Request, - res: Response, - next: NextFunction - ) => Promise>>; -} - -export interface AuthenticationServiceInterface { - login(): Promise; - signup(): Promise; - logout(): Promise; -} - -export interface UserRepositoryInterface { - createUser(user: UserInterface): Promise; - getUserByEmail(email: string): Promise; -} - -export interface UserInterface { - getEmail(): string; - getPassword(): string; - getFirstName(): string; - getLastName(): string; - setEmail(email: string): void; - setPassword(password: string): void; - setFirstName(firstName: string): void; - setLastName(lastName: string): void; -} - -export interface PrismaUserInterface { - id: string; - email: string; - password: string; - firstName: string; - lastName: string; - createdAt: Date; - updatedAt: Date; -}