From de64cbd09120b461c2d8fde2c6e9babdf1cf6c82 Mon Sep 17 00:00:00 2001 From: Oluwakorede Fashokun Date: Thu, 2 Jan 2025 17:09:10 -0500 Subject: [PATCH] More datalayer preparations --- api/src/controllers/products.ts | 20 +++++++ api/src/controllers/stores.ts | 21 ++++---- api/src/controllers/users.ts | 6 ++- api/src/utils/queries.ts | 63 ++++++++++++++++++++++ apps/app/src/data/carts.ts | 12 ++--- apps/app/src/data/filters.ts | 95 +++++++++++++++++++++++++++++++++ apps/app/src/data/queries.ts | 7 --- apps/app/src/data/types.ts | 6 +++ 8 files changed, 202 insertions(+), 28 deletions(-) create mode 100644 api/src/utils/queries.ts create mode 100644 apps/app/src/data/filters.ts diff --git a/api/src/controllers/products.ts b/api/src/controllers/products.ts index 89fe8687..ba86b662 100644 --- a/api/src/controllers/products.ts +++ b/api/src/controllers/products.ts @@ -98,4 +98,24 @@ export default class ProductController { return res.json({ review }); } + + // PUT /products/:id + public async updateProduct(req: Request, res: Response) { + if (!req.auth) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (!req.params.id) { + return res.status(400).json({ error: 'Product ID is required' }); + } + + const { name, description, unitPrice, quantity } = req.body; + + const product = await prismaClient.product.update({ + where: { id: req.params.id }, + data: { name, description, unitPrice, quantity } + }); + + return res.json({ product }); + } } diff --git a/api/src/controllers/stores.ts b/api/src/controllers/stores.ts index dc413460..23a2ea28 100644 --- a/api/src/controllers/stores.ts +++ b/api/src/controllers/stores.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import prismaClient from '../config/prisma'; +import { hydrateQuery } from '../utils/queries'; import { uploadImages } from '../utils/upload'; export default class StoreController { @@ -40,13 +41,11 @@ export default class StoreController { // GET /stores/:id/products public async getStoreProducts(req: Request, res: Response) { + const query = hydrateQuery(req.query); + const products = await prismaClient.store - .findUnique({ - where: { - id: req.headers['x-market-store-id'] as string - } - }) - .products(); + .findUnique({ where: { id: req.headers['x-market-store-id'] as string } }) + .products(query); return res.json({ products }); } @@ -85,13 +84,11 @@ export default class StoreController { // GET /stores/:id/orders public async getStoreOrders(req: Request, res: Response) { + const query = hydrateQuery(req.query); + const orders = await prismaClient.store - .findUnique({ - where: { - id: req.headers['x-market-store-id'] as string - } - }) - .orders(); + .findUnique({ where: { id: req.headers['x-market-store-id'] as string } }) + .orders(query); return res.json({ orders }); } diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index 5ae2961a..f26a7555 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import prismaClient from '../config/prisma'; +import { hydrateQuery } from '../utils/queries'; export default class UserController { // GET /users/current @@ -25,12 +26,15 @@ export default class UserController { // GET /users/current/orders public async getOrders(req: Request, res: Response) { + const query = hydrateQuery(req.query); + if (!req.auth) { return res.status(401).json({ error: 'User not authenticated' }); } const orders = await prismaClient.order.findMany({ - where: { userId: req.auth.id } + where: { userId: req.auth.id }, + ...query }); return res.json({ orders }); diff --git a/api/src/utils/queries.ts b/api/src/utils/queries.ts new file mode 100644 index 00000000..bbe7ce9b --- /dev/null +++ b/api/src/utils/queries.ts @@ -0,0 +1,63 @@ +import { Request } from 'express'; + +type FilterOperators = { + equals?: any; + in?: any[]; + notIn?: any[]; + contains?: string; + startsWith?: string; + endsWith?: string; + mode?: 'insensitive'; + lt?: number; + lte?: number; + gt?: number; + gte?: number; +}; + +export const hydrateQuery = (query: Request['query']) => { + const filter: Record = {}; + const orderBy: Record = {}; + + // Process each query parameter + Object.entries(query).forEach(([key, value]) => { + // Normalize the value to string or string[] + const normalizedValue = Array.isArray(value) + ? value.map(v => v.toString()) + : value?.toString(); + + if (!normalizedValue) return; + + // Handle orderBy parameters + if (key.startsWith('orderBy[')) { + const field = key.slice(8, -1); + orderBy[field] = normalizedValue as 'asc' | 'desc'; + return; + } + + // Rest of the function remains the same... + const filterMatch = key.match(/^(\w+)\[(\w+)\]$/); + if (filterMatch) { + const [, field, operator] = filterMatch; + if (!field || !operator) return; + + if (!filter[field]) { + filter[field] = {}; + } + + if (Array.isArray(normalizedValue)) { + filter[field][operator] = normalizedValue.map(v => + !isNaN(Number(v)) ? Number(v) : v + ); + } else { + filter[field][operator] = !isNaN(Number(normalizedValue)) + ? Number(normalizedValue) + : normalizedValue; + } + } + }); + + return { + ...(Object.keys(filter).length > 0 && { where: filter }), + ...(Object.keys(orderBy).length > 0 && { orderBy }) + }; +}; diff --git a/apps/app/src/data/carts.ts b/apps/app/src/data/carts.ts index 3d9dad18..6e886d2c 100644 --- a/apps/app/src/data/carts.ts +++ b/apps/app/src/data/carts.ts @@ -24,7 +24,7 @@ export default class CartService { } public getCarts() { - return this.api.get('/carts'); + return this.api.get('/users/current/carts'); } public getCart(cartId: string) { @@ -32,18 +32,14 @@ export default class CartService { } public addProductToCart(body: AddProductToCartBody) { - return this.api.post(`/carts/${body.storeId}/products`, body); + return this.api.post(`/carts/products`, body); } - public removeProductFromCart(body: AddProductToCartBody) { - return this.api.delete(`/carts/${body.storeId}/products/${body.productId}`); + public removeProductFromCart(cartId: string, productId: string) { + return this.api.delete(`/carts/${cartId}/products/${productId}`); } public updateCartProduct(productId: string, body: UpdateCartProductBody) { return this.api.patch(`/carts/${body.cartId}/products/${productId}`, body); } - - public removeFromCart(productId: string, cartId: string) { - return this.api.delete(`/carts/${cartId}/products/${productId}`); - } } diff --git a/apps/app/src/data/filters.ts b/apps/app/src/data/filters.ts new file mode 100644 index 00000000..9456c25d --- /dev/null +++ b/apps/app/src/data/filters.ts @@ -0,0 +1,95 @@ +import { Sort } from '../types/api'; + +type StringFilter = { + equals?: string; + in?: string[]; + notIn?: string[]; + contains?: string; + startsWith?: string; + endsWith?: string; + mode?: 'insensitive'; +}; + +type IntFilter = { + equals?: number; + in?: number[]; + notIn?: number[]; + lt?: number; + lte?: number; + gt?: number; + gte?: number; +}; + +type ProductFilter = { + name?: StringFilter; + description?: StringFilter; + unitPrice?: IntFilter; + quantity?: IntFilter; +}; + +type ProductOrderBy = { + createdAt?: Sort; + updatedAt?: Sort; + unitPrice?: Sort; +}; + +type OrderFilter = { + total?: IntFilter; +}; + +type OrderOrderBy = { + total?: Sort; + createdAt?: Sort; + updatedAt?: Sort; +}; + +export const buildQuery = < + T extends Record, + U extends Record +>({ + filter, + orderBy +}: { + filter?: T; + orderBy?: U; +}) => { + const params = new URLSearchParams(); + + // Handle filters + if (filter) { + Object.entries(filter).forEach(([field, conditions]) => { + Object.entries(conditions || {}).forEach(([operator, value]) => { + if (value !== undefined) { + if (Array.isArray(value)) { + value.forEach(v => + params.append(`${field}[${operator}]`, v.toString()) + ); + } else { + params.append(`${field}[${operator}]`, value.toString()); + } + } + }); + }); + } + + // Handle ordering + if (orderBy) { + Object.entries(orderBy).forEach(([field, direction]) => { + if (direction) { + params.append(`orderBy[${field}]`, direction); + } + }); + } + + return params.toString(); +}; + +export const buildProductQuery = (params: { + filter?: ProductFilter; + orderBy?: ProductOrderBy; +}) => buildQuery(params); + +export const buildOrderQuery = (params: { + filter?: OrderFilter; + orderBy?: OrderOrderBy; +}) => buildQuery(params); diff --git a/apps/app/src/data/queries.ts b/apps/app/src/data/queries.ts index 18a66ef1..8b16236b 100644 --- a/apps/app/src/data/queries.ts +++ b/apps/app/src/data/queries.ts @@ -27,13 +27,6 @@ export const useOrderQuery = (orderId: string) => { }; // Product Queries -export const useProductsQuery = (filter?: any, orderBy?: any) => { - return useQuery({ - queryKey: ['products', filter, orderBy], - queryFn: () => dataService.products.getProducts({ filter, orderBy }) - }); -}; - export const useProductQuery = (productId: string) => { return useQuery({ queryKey: ['products', productId], diff --git a/apps/app/src/data/types.ts b/apps/app/src/data/types.ts index 8219ec21..f6b73add 100644 --- a/apps/app/src/data/types.ts +++ b/apps/app/src/data/types.ts @@ -103,6 +103,8 @@ export interface WatchlistProduct { id: string; productId: string; userId: string; + product: Product; + user: User; createdAt: string; updatedAt: string; } @@ -112,6 +114,8 @@ export interface Image { storeId: string; productId: string; path: string; + store: Store; + product: Product; createdAt: string; updatedAt: string; } @@ -120,6 +124,8 @@ export interface StoreManager { id: string; storeId: string; managerId: string; + store: Store; + manager: User; createdAt: string; updatedAt: string; }