Skip to content

Commit

Permalink
searchProducts (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dawaic6 authored May 27, 2024
1 parent d66c4f1 commit 48b2d73
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 2 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/src/emails/",
"/src/middlewares/"
"/src/emails/"
],
"testPathIgnorePatterns": [
"/node_modules/",
Expand Down
47 changes: 47 additions & 0 deletions src/__test__/searchProduct.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import request from 'supertest';
import app from '../app';
import { beforeAllHook, afterAllHook, getBuyerToken } from './testSetup';

beforeAll(beforeAllHook);
afterAll(afterAllHook);

describe('Search Products Controller Test', () => {
let buyerToken: string;

beforeAll(async () => {
buyerToken = await getBuyerToken();
});

it('should search products with keyword', async () => {

const response = await request(app)
.get('/api/v1/search?keyword=keyword')
.set('Authorization', `Bearer ${buyerToken}`);
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
});

it('should search products by category', async () => {
const response = await request(app)
.get('/api/v1/search?category=categoryName')
.set('Authorization', `Bearer ${buyerToken}`);
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
});

it('should search products by product name', async () => {
const response = await request(app)
.get('/api/v1/search?productName=productName')
.set('Authorization', `Bearer ${buyerToken}`);
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
});

it('should search products and apply sorting', async () => {
const response = await request(app)
.get('/api/v1/search?sort=asc')
.set('Authorization', `Bearer ${buyerToken}`);
expect(response.status).toBe(200);
expect(response.body.data).toBeDefined();
});
});
3 changes: 3 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import authRoutes from './routes/auth-routes';
import cookieSession from 'cookie-session';
import passport from 'passport';
import userRouter from './routes/userRoutes';
import searchRoutes from './routes/searchRoutes'
// Require Passport midleware
import './middlewares/passport-setup';

Expand Down Expand Up @@ -79,6 +80,8 @@ app.get('/', (req: Request, res: Response) => {
// Middleware to handle all endpoint routes
app.use('/api/v1', router);
app.use('/api/v1', userRouter);
app.use('/api/v1', searchRoutes);

// Endpoints for serving social login
app.use('/auth', authRoutes);

Expand Down
54 changes: 54 additions & 0 deletions src/controller/searchProducts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Request, Response } from 'express';
import dbConnection from '../database';
import Product from '../database/models/productEntity';
import Category from '../database/models/categoryEntity';
import errorHandler from '../middlewares/errorHandler';

const productRepository = dbConnection.getRepository(Product);
const categoryRepository = dbConnection.getRepository(Category);

export const searchProducts = errorHandler(
async (req: Request, res: Response) => {
const { keyword, category, productName, sort } = req.query;

let queryBuilder = productRepository.createQueryBuilder('product');

if (keyword) {
queryBuilder = queryBuilder.andWhere(
'product.name ILIKE :keyword OR product.shortDesc ILIKE :keyword OR product.longDesc ILIKE :keyword',
{ keyword: `%${keyword}%` }
);
}

if (category) {
const categoryEntity = await categoryRepository.findOne({
where: { name: category as string },
});
if (categoryEntity) {
queryBuilder = queryBuilder.andWhere(
'product.categoryId = :categoryId',
{ categoryId: categoryEntity.id }
);
}
}

if (productName) {
queryBuilder = queryBuilder.andWhere('product.name ILIKE :productName', {
productName: `%${productName}%`,
});
}

if (sort) {
const sortDirection = sort.toString().toUpperCase() as 'ASC' | 'DESC';
queryBuilder = queryBuilder.orderBy('product.salesPrice', sortDirection);
}

const total = await queryBuilder.getCount();
const products = await queryBuilder.getMany();

return res.status(200).json({
data: products,
total,
});
}
);
34 changes: 34 additions & 0 deletions src/docs/searchProduct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @swagger
* /api/v1/search:
* get:
* summary: Search products
* tags: [Buyer]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: keyword
* type: string
* description: Keyword to search for in product name, short description, or long description.
* - in: query
* name: category
* type: string
* description: Name of the category to filter the products.
* - in: query
* name: productName
* type: string
* description: Name of the product to filter the products.
* - in: query
* name: sort
* type: string
* enum: [asc, desc]
* description: Sort order for the products based on sales price.
* responses:
* '200':
* description: Successful operation. Returns the list of products matching the search criteria.
* '400':
* description: Invalid search parameters provided.
* '500':
* description: Internal Server Error.
*/
16 changes: 16 additions & 0 deletions src/middlewares/validateSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { query, validationResult } from 'express-validator';
import { Request, Response, NextFunction } from 'express';
export const validateSearchParams = [
query('keyword').optional().isString().withMessage('Keyword must be a string'),
query('category').optional().isString().withMessage('Category must be a string'),
query('brand').optional().isString().withMessage('Brand must be a string'),
query('productName').optional().isString().withMessage('Product name must be a string'),
query('sort').optional().isIn(['asc', 'desc']).withMessage('Sort order must be either asc or desc'),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
10 changes: 10 additions & 0 deletions src/routes/searchRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from 'express';
import { searchProducts } from '../controller/searchProducts'
import { validateSearchParams } from '../middlewares/validateSearchParams';
import { IsLoggedIn } from '../middlewares/isLoggedIn';
import { checkRole } from '../middlewares/authorize';
const searchRouter = Router();

searchRouter.get('/search', IsLoggedIn,checkRole(['Buyer']), validateSearchParams, searchProducts);

export default searchRouter;

0 comments on commit 48b2d73

Please sign in to comment.