Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #60

Merged
merged 27 commits into from
Feb 5, 2024
Merged

Dev #60

Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0828b27
Updating postProduct and createProduct to include product url
ryanjung1998 Jan 28, 2024
e174bcb
Merge pull request #50 from techstartucalgary/api
Axeloooo Jan 28, 2024
df52363
Scanner Endpoint, minimal error checking
ryanjung1998 Feb 3, 2024
8360245
Merge pull request #54 from techstartucalgary/scanner
Axeloooo Feb 3, 2024
7751041
fix: Data type in schema.primsa updated
Axeloooo Feb 2, 2024
55d0470
fix: Tables for databases cleaned up and updated
Axeloooo Feb 4, 2024
4b0c5f9
build: Mysql container added for development
Axeloooo Feb 4, 2024
f30974c
feat: Scanner feature cleaned up and improved functions
Axeloooo Feb 4, 2024
be097ba
feat: Tesseract custom error class implemented
Axeloooo Feb 4, 2024
68736b4
fix: Product provider issues fixed and algorithm feature deleted
Axeloooo Feb 4, 2024
39fff2a
fix: Scanner functions in service and repository classes explicitly t…
Axeloooo Feb 4, 2024
abb239e
fix: eng.traineddata file deleted
Axeloooo Feb 4, 2024
3b98dc4
Merge pull request #55 from techstartucalgary/feature/backend-scanner
Axeloooo Feb 4, 2024
e4634fe
feat: Zode validations added for scanner and product endpoints
Axeloooo Feb 4, 2024
446909b
feat: Zod validations added for scanner and product endpoints
Axeloooo Feb 4, 2024
56e2b64
lint: Eslint added for linting
Axeloooo Feb 4, 2024
858c952
fix: Issues with the request types infered by zod in controllers reso…
Axeloooo Feb 4, 2024
84cfb75
Merge pull request #57 from techstartucalgary/fix/controllers-return-…
Axeloooo Feb 4, 2024
8d4cd53
ci: Linting workflow implemented
Axeloooo Feb 4, 2024
bf26383
Merge branch 'dev' into ci/linting-workflow
Axeloooo Feb 4, 2024
6feb20f
Merge pull request #58 from techstartucalgary/ci/linting-workflow
Axeloooo Feb 4, 2024
87db61d
test: Unit testing for products services created
Axeloooo Feb 5, 2024
ad82c07
test: Unit testing for scanner service created
Axeloooo Feb 5, 2024
3f99f23
ci: Testing workflow added
Axeloooo Feb 5, 2024
acfcddb
build: Dependencies cleaned up and documentation added in package.json
Axeloooo Feb 5, 2024
83e8335
fix: Issue with typo in test workflow resolved
Axeloooo Feb 5, 2024
850b0ca
Merge pull request #59 from techstartucalgary/build/dependency-update
Axeloooo Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: Zode validations added for scanner and product endpoints
Axeloooo committed Feb 4, 2024
commit e4634fe9b2fccff3e52ec0f4c4fedf1944d0e7f6
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@
- [Anfaal]() - Backend Developer
- [Ryan]() - Backend Developer
- [Alison]() - Backend Developer

## 👨‍💻 Tech Stack

- Frontend
@@ -51,12 +51,12 @@
![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)
![Zod](https://img.shields.io/badge/Zod-3E67B1.svg?style=for-the-badge&logo=Zod&logoColor=white)

- Cloud

![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/).
@@ -187,39 +187,40 @@ datasource db {
npx prisma db push
```


# 🌟 Frontend Documentation

The frontend is crafted for iOS platforms, utilizing Swift and SwiftUI. The code is primarily housed in the `Rethread` directory. This section details the setup, development practices, and testing for the frontend environment.

## 🏃 Quickstart

1. **Clone the Repository**:
```bash
git clone [email protected]:techstartucalgary/fashion.git
```

```bash
git clone [email protected]:techstartucalgary/fashion.git
```

2. **Navigate to the Frontend Directory**:
```bash
cd Rethread
```

```bash
cd Rethread
```

3. **Open the Project in Xcode**:
Open the project file `.xcodeproj` in Xcode.
Open the project file `.xcodeproj` in Xcode.

4. **Run the Application**:
Select an iOS simulator or connected device in Xcode and click 'Run'.
Select an iOS simulator or connected device in Xcode and click 'Run'.

## 🛠️ Setup and Installation

1. **Install Xcode**:
Ensure you have Xcode installed on your macOS, available through the Mac App Store.
Ensure you have Xcode installed on your macOS, available through the Mac App Store.

2. **Update Swift and SwiftUI**:
Ensure you have the latest version of Swift and SwiftUI installed, as they are crucial for frontend development.
Ensure you have the latest version of Swift and SwiftUI installed, as they are crucial for frontend development.

3. **Verify the Installation**:
Open Xcode and check for Swift and SwiftUI updates in the preferences.
Open Xcode and check for Swift and SwiftUI updates in the preferences.

4. **Minimum iOS Version**: This app is built for `iOS 16` and above.

23 changes: 9 additions & 14 deletions backend/src/abstracts/product.abstract.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { PrismaProduct, PrismaProducts } from "../../types";
import {
PrismaProduct,
PrismaProducts,
CreateProduct,
GetProduct,
} from "../types";

abstract class ProductProvider {
abstract getProducts(): Promise<PrismaProducts>;

abstract getProductById(id: string): Promise<PrismaProduct>;
abstract getProductById(getProduct: GetProduct): Promise<PrismaProduct>;

abstract createProduct(
title: string,
size: string,
color: string,
description: string,
gender: string,
category: string,
price: number,
imageUrl: string,
url: string
): Promise<PrismaProduct>;
abstract createProduct(createProduct: CreateProduct): Promise<PrismaProduct>;

abstract deleteProduct(id: string): Promise<PrismaProduct>;
abstract deleteProduct(getProduct: GetProduct): Promise<PrismaProduct>;
}

export default ProductProvider;
4 changes: 2 additions & 2 deletions backend/src/abstracts/scanner.abstract.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Tag } from "../../types";
import { ScannerRequest, Tag } from "../types";

abstract class ScannerProvider {
abstract getMaterials(text: string): Tag[];

abstract getTextFromImage(imagePath: string): Promise<string>;
abstract getTextFromImage(scannerRequest: ScannerRequest): Promise<string>;
}

export default ScannerProvider;
24 changes: 7 additions & 17 deletions backend/src/controllers/product.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaProduct } from "../../types";
import { CreateProduct, GetProduct, PrismaProduct } from "../types";
import ProductProvider from "../abstracts/product.abstract";
import { Request, Response, NextFunction } from "express";

@@ -21,13 +21,13 @@ class ProductController {
};

public getProductById = async (
req: Request,
req: Request<GetProduct>,
res: Response,
next: NextFunction
): Promise<Response<any, Record<string, any>> | void> => {
try {
const product: PrismaProduct = await this.service.getProductById(
req.params.id
req.params
);
return res.status(200).json(product);
} catch (e) {
@@ -36,35 +36,25 @@ class ProductController {
};

public postProduct = async (
req: Request,
req: Request<unknown, unknown, CreateProduct>,
res: Response,
next: NextFunction
): Promise<Response<any, Record<string, any>> | 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,
req.body.url
);
const newProduct = await this.service.createProduct(req.body);
return res.status(201).json(newProduct);
} catch (e) {
next(e);
}
};

public deleteProduct = async (
req: Request,
req: Request<GetProduct>,
res: Response,
next: NextFunction
): Promise<Response<any, Record<string, any>> | void> => {
try {
const product = await this.service.deleteProduct(req.params.id);
const product = await this.service.deleteProduct(req.params);
return res.status(200).json(product);
} catch (e) {
next(e);
6 changes: 3 additions & 3 deletions backend/src/controllers/scanner.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Tag } from "../../types.js";
import { ScannerRequest, Tag } from "../types.js";
import ScannerProvider from "../abstracts/scanner.abstract.js";
import { Request, Response, NextFunction } from "express";

@@ -8,12 +8,12 @@ class ScannerController {
}

public postMaterials = async (
req: Request,
req: Request<unknown, unknown, ScannerRequest>,
res: Response,
next: NextFunction
): Promise<Response<any, Record<string, any>> | void> => {
try {
const text: string = await this.service.getTextFromImage(req.body.image);
const text: string = await this.service.getTextFromImage(req.body);
const materials: Tag[] = this.service.getMaterials(text);
return res.status(201).json(materials);
} catch (e) {
8 changes: 5 additions & 3 deletions backend/src/middlewares/error.middleware.ts
Original file line number Diff line number Diff line change
@@ -15,12 +15,12 @@ import {
import { ProductNotFoundError } from "../errors/product.error.js";
import { TesseractServiceError } from "../errors/tesseract.error.js";

export default function errorHandler(
const errorHandler = (
e: Error,
_: Request,
res: Response,
next: NextFunction
) {
) => {
if (e instanceof HttpBadRequestError) {
res.status(400).json({ error: "Bad Request Error" });
} else if (e instanceof HttpUnauthorizedError) {
@@ -46,4 +46,6 @@ export default function errorHandler(
} else {
res.status(500).json({ error: "Unexpected error" });
}
}
};

export default errorHandler;
21 changes: 21 additions & 0 deletions backend/src/middlewares/schemaValidation.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NextFunction, Request, Response } from "express";
import { AnyZodObject, ZodError } from "zod";

const schemaValidation =
(schemaValidation: AnyZodObject) =>
(req: Request, res: Response, next: NextFunction) => {
try {
schemaValidation.parse(req);
next();
} catch (e) {
if (e instanceof ZodError) {
res.status(400).json({
error: e.errors.map((error) => error.message),
});
} else {
next(e);
}
}
};

export default schemaValidation;
44 changes: 23 additions & 21 deletions backend/src/repositories/product.repository.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,12 @@ import ProductProvider from "../abstracts/product.abstract.js";
import { prisma } from "../index.js";
import { Prisma } from "@prisma/client";
import { ProductNotFoundError } from "../errors/product.error.js";
import { PrismaProduct, PrismaProducts } from "../../types.js";
import {
CreateProduct,
GetProduct,
PrismaProduct,
PrismaProducts,
} from "../types.js";
import {
PrismaClientInitializationError,
PrismaClientRustPanicError,
@@ -33,10 +38,12 @@ class ProductRepository implements ProductProvider {
}
};

public getProductById = async (id: string): Promise<PrismaProduct> => {
public getProductById = async (
getProduct: GetProduct
): Promise<PrismaProduct> => {
try {
const product: PrismaProduct | null = await prisma.product.findUnique({
where: { id: id },
where: { id: getProduct.params.id },
});
if (product === null) {
throw new ProductNotFoundError();
@@ -62,26 +69,19 @@ class ProductRepository implements ProductProvider {
};

public createProduct = async (
title: string,
size: string,
color: string,
description: string,
gender: string,
category: string,
price: number,
imageUrl: string
createProduct: CreateProduct
): Promise<PrismaProduct> => {
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,
title: createProduct.body.title,
size: createProduct.body.size,
color: createProduct.body.color,
description: createProduct.body.description,
gender: createProduct.body.gender,
category: createProduct.body.category,
price: createProduct.body.price,
imageUrl: createProduct.body.imageUrl,
},
});
return newProduct;
@@ -102,10 +102,12 @@ class ProductRepository implements ProductProvider {
}
};

public deleteProduct = async (id: string): Promise<PrismaProduct> => {
public deleteProduct = async (
getProduct: GetProduct
): Promise<PrismaProduct> => {
try {
const product: PrismaProduct | null = await prisma.product.delete({
where: { id: id },
where: { id: getProduct.params.id },
});
if (product === null) {
throw new ProductNotFoundError();
14 changes: 9 additions & 5 deletions backend/src/repositories/scanner.repository.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { createWorker } from "tesseract.js";
import { Tag } from "../../types.js";
import ScannerProvider from "../abstracts/scanner.abstract.js";
import { createWorker } from "tesseract.js";
import { ScannerRequest, Tag } from "../types.js";
import { TesseractServiceError } from "../errors/tesseract.error.js";

class ScannerRepository implements ScannerProvider {
public getMaterials = (_: string): Tag[] => {
public getMaterials = (text: string): Tag[] => {
throw new Error("Method not implemented.");
};

public getTextFromImage = async (imagePath: string): Promise<string> => {
public getTextFromImage = async (
scannerRequest: ScannerRequest
): Promise<string> => {
try {
const worker: Tesseract.Worker = await createWorker("eng");
const ret: Tesseract.RecognizeResult = await worker.recognize(imagePath);
const ret: Tesseract.RecognizeResult = await worker.recognize(
scannerRequest.body.imageUrl
);
await worker.terminate();
return ret.data.text;
} catch (e) {
26 changes: 23 additions & 3 deletions backend/src/routes/product.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import ProductController from "../controllers/product.controller.js";
import ProductService from "../services/product.service.js";
import ProductRepository from "../repositories/product.repository.js";
import schemaValidation from "../middlewares/schemaValidation.middleware.js";
import {
GetProductSchema,
CreateProductSchema,
} from "../schemas/product.schema.js";
import { Router } from "express";

const productRouter = Router();
@@ -9,8 +14,23 @@ const productController = new ProductController(
);

productRouter.get("/", productController.getProducts);
productRouter.get("/:id", productController.getProductById);
productRouter.post("/", productController.postProduct);
productRouter.delete("/:id", productController.deleteProduct);

productRouter.get(
"/:id",
schemaValidation(GetProductSchema),
productController.getProductById
);

productRouter.post(
"/",
schemaValidation(CreateProductSchema),
productController.postProduct
);

productRouter.delete(
"/:id",
schemaValidation(GetProductSchema),
productController.deleteProduct
);

export default productRouter;
8 changes: 7 additions & 1 deletion backend/src/routes/scanner.routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import ScannerController from "../controllers/scanner.controller.js";
import ScannerService from "../services/scanner.service.js";
import ScannerRepository from "../repositories/scanner.repository.js";
import schemaValidation from "../middlewares/schemaValidation.middleware.js";
import { Router } from "express";
import { scannerSchema } from "../schemas/scanner.schema.js";

const scannerRouter = Router();
const scannerController = new ScannerController(
new ScannerService(new ScannerRepository())
);

scannerRouter.post("/", scannerController.postMaterials);
scannerRouter.post(
"/",
schemaValidation(scannerSchema),
scannerController.postMaterials
);

export default scannerRouter;
21 changes: 21 additions & 0 deletions backend/src/schemas/product.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from "zod";

export const CreateProductSchema = z.object({
body: z.object({
title: z.string(),
size: z.string(),
color: z.string(),
description: z.string(),
gender: z.string(),
category: z.string(),
price: z.number().nonnegative(),
imageUrl: z.string().url(),
url: z.string().url(),
}),
});

export const GetProductSchema = z.object({
params: z.object({
id: z.string().uuid(),
}),
});
7 changes: 7 additions & 0 deletions backend/src/schemas/scanner.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const ScannerSchema = z.object({
body: z.object({
imageUrl: z.string().url(),
}),
});
58 changes: 16 additions & 42 deletions backend/src/services/product.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import ProductProvider from "../abstracts/product.abstract.js";
import { PrismaProduct, PrismaProducts } from "../../types.js";
import { HttpBadRequestError } from "../errors/http.error.js";
import {
CreateProduct,
GetProduct,
PrismaProduct,
PrismaProducts,
} from "../types.js";

class ProductService implements ProductProvider {
constructor(private provider: ProductProvider) {
@@ -11,52 +15,22 @@ class ProductService implements ProductProvider {
return await this.provider.getProducts();
};

public getProductById = async (id: string): Promise<PrismaProduct> => {
if (!id) {
throw new HttpBadRequestError();
}
return await this.provider.getProductById(id);
public getProductById = async (
getProduct: GetProduct
): Promise<PrismaProduct> => {
return await this.provider.getProductById(getProduct);
};

public createProduct = async (
title: string,
size: string,
color: string,
description: string,
gender: string,
category: string,
price: number,
imageUrl: string,
url: string
createProduct: CreateProduct
): Promise<PrismaProduct> => {
if (
!title ||
!size ||
!color ||
!description ||
!gender ||
!category ||
!price ||
!imageUrl ||
!url
) {
throw new HttpBadRequestError();
}
return await this.provider.createProduct(
title,
size,
color,
description,
gender,
category,
price,
imageUrl,
url
);
return await this.provider.createProduct(createProduct);
};

public deleteProduct = async (id: string): Promise<PrismaProduct> => {
return await this.provider.deleteProduct(id);
public deleteProduct = async (
getProduct: GetProduct
): Promise<PrismaProduct> => {
return await this.provider.deleteProduct(getProduct);
};
}

12 changes: 5 additions & 7 deletions backend/src/services/scanner.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ScannerProvider from "../abstracts/scanner.abstract.js";
import { Tag } from "../../types.js";
import { HttpBadRequestError } from "../errors/http.error.js";
import { ScannerRequest, Tag } from "../types.js";

class ScannerService implements ScannerProvider {
constructor(private provider: ScannerProvider) {
@@ -24,11 +23,10 @@ class ScannerService implements ScannerProvider {
return scannedTags;
};

public getTextFromImage = async (imagePath: string): Promise<string> => {
if (!imagePath) {
throw new HttpBadRequestError();
}
return await this.provider.getTextFromImage(imagePath);
public getTextFromImage = async (
scannerRequest: ScannerRequest
): Promise<string> => {
return await this.provider.getTextFromImage(scannerRequest);
};
}

33 changes: 33 additions & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { z } from "zod";
import {
CreateProductSchema,
GetProductSchema,
} from "./schemas/product.schema.js";
import { ScannerSchema } from "./schemas/scanner.schema.js";

export type PrismaProduct = {
id: string;
title: string;
size: string;
color: string;
description: string;
gender: string;
category: string;
price: number;
imageUrl: string;
createdAt: Date;
updatedAt: Date;
};

export type PrismaProducts = PrismaProduct[];

export type Tag = {
material: string;
percentage: string;
};

export type CreateProduct = z.infer<typeof CreateProductSchema>;

export type GetProduct = z.infer<typeof GetProductSchema>;

export type ScannerRequest = z.infer<typeof ScannerSchema>;