Product Catalog System - Deep Dive Documentation¶
Generated: 2026-04-11 Scope: Full-stack Product Catalog — Backend Product/ProductCategory/Image modules + entities + frontend pages/components/API hooks Files Analyzed: 23 (13 backend + 10 frontend) Lines of Code: ~2,786 Workflow Mode: Exhaustive Deep-Dive Severity Breakdown: 18 Critical, 17 High, 22 Medium, 19 Low — 76 total issues
Overview¶
The Product Catalog system is a full-stack e-commerce feature consisting of Product CRUD with multi-image support, ProductCategory management, and a standalone Image upload module. The backend is functionally complete (endpoints exist, entities defined, validation schemas present) but the frontend is entirely non-functional — every API call is commented out and replaced with hardcoded mock data and simulated delays.
Purpose: Enable SaaS tenants to manage a product catalog with categories, pricing, discount pricing, multi-image uploads, and search/filter capabilities.
Key Responsibilities: - Product CRUD with multi-image upload (up to 10 images via multer) - Product category management (name uniqueness, CRUD) - Standalone image upload endpoint - Public product browsing (GET endpoints are unauthenticated) - Dashboard product management with RBAC permission gates
Integration Points:
- handleFileUpload / saveBufferToFile helpers for image storage (Cloudinary/S3/local)
- checkPermissionAndThrow for RBAC enforcement
- getDbRepository shared utility for TypeORM access
- catchAsync error wrapper on all controllers
- sendResponse standardized response formatter
- validateRequest Zod middleware for request validation
- PermissionWrapper frontend component for UI permission gating
Critical Architecture Finding¶
The entire Product Catalog frontend is a UI mockup with zero backend integration.
Every product CRUD operation (list, create, edit, delete) uses hardcoded mock data and setTimeout delays to simulate API calls. The only functional frontend integration is the Product Categories page, which makes real API calls via axios. This means:
- Users cannot create, edit, or delete products through the UI
- The products list always shows the same 15 hardcoded items
- The edit page always loads "Wireless Bluetooth Headphones" regardless of product ID
- Delete operations only remove items from React local state (lost on page refresh)
- The backend API is fully functional but never called
Known Issues Summary¶
Critical (18)¶
| # | Location | Issue | Impact |
|---|---|---|---|
| C1 | product.validation.ts:15-21 |
DUAL field mismatch: schema uses originalPrice (z.string()) but entity has discountPrice (decimal(10,2)) — wrong name AND wrong type. Additionally, Zod's default z.object() strips unknown keys and validateRequest overwrites req.body with the stripped result, so even sending the correct field name discountPrice from the frontend would be silently stripped before reaching the controller |
Data loss at two stages: (1) Zod strips discountPrice as unknown key, (2) TypeORM ignores originalPrice (no matching column) |
| C2 | product.service.ts:103 |
updateProduct permission check commented out |
Any authenticated user can update any product |
| C3 | product.service.ts:119-129 |
deleteProductById has zero permission check — complete RBAC bypass |
Any authenticated user regardless of role (viewer, editor, subscriber) can soft-delete any product |
| C4 | product.service.ts:35,76,122 |
All queries filter isDeleted: false but ignore @DeleteDateColumn |
Products soft-deleted via TypeORM deletedAt still appear in queries |
| C5 | product_category.routes.ts:3,18,24 |
Routes import BlogCategoryValidation instead of ProductCategoryValidation |
Wrong validation rules applied to all category create/update requests |
| C6 | product_category.service.ts:104 |
Type cast to DeepPartial<BlogCategory> instead of ProductCategory |
TypeScript type error; wrong entity type in update logic |
| C7 | product_category.service.ts:125 |
Uses repo.remove() (hard delete) despite entity having @DeleteDateColumn |
Permanent data loss — categories irrecoverably deleted |
| C8 | seed/data/permission.ts |
Zero product permissions seeded (products.create, product-category.*) |
All checkPermissionAndThrow calls will deny access for non-super_admin |
| C9 | products-page.jsx:381-408 |
Product list uses hardcoded mock data; API call commented out | Products page non-functional — always shows same 15 items |
| C10 | create/page.js:13-25 |
Create product API call entirely commented out | Product creation non-functional |
| C11 | [id]/page.js:25-35 |
Edit product fetch entirely commented out; uses mock data | Edit page loads same mock product for all IDs |
| C12 | [id]/page.js:84-95 |
Edit product save API call entirely commented out | Product editing non-functional |
| C13 | products-page.jsx:451-455,562-566 |
Delete/bulk delete only update local React state | Products reappear on page refresh |
| C14 | product-performance.jsx |
Component named "ProductPerformance" but displays project/task data | Dashboard card shows irrelevant data ("Minecraf App", "Web App Project") |
| C15 | api/products/index.js |
File is completely empty (0 exports) | No React Query hooks for product CRUD exist |
| C16 | product.controller.ts:122-126 |
updateProduct returns HTTP 201 with message "Product created successfully" |
Wrong status code (should be 200) and wrong message (should say "updated") |
| C17 | product.validation.ts:9 |
Images array validates .url() which rejects local paths /uploads/products/... |
Local storage mode will always fail Zod validation |
| C18 | product.service.ts:91 |
Permission string "products.create" uses plural; other modules use singular |
Inconsistent permission naming; will never match seed data if added |
High (17)¶
| # | Location | Issue |
|---|---|---|
| H1 | product_category.routes.ts:12 |
ALL category routes require auth — no public read for product filters |
| H2 | product_category.controller.ts:13,24,38,67,81 |
5 response messages say "Blog" instead of "Product" |
| H3 | product_category.service.ts:18,30,53 |
No .view permission check on read operations |
| H4 | image.route.ts:9 |
Image upload endpoint has no auth middleware — public upload |
| H5 | image.service.ts:11 |
Storage hardcoded to "cloudinary" — ignores AppConfig setting |
| H6 | Product.ts:24-25 |
Image array column has no URL format validation or array size limit |
| H7 | Product.ts:45-46 |
No DB constraint that discountPrice < price |
| H8 | product.controller.ts:39-42 |
Cloud image uploads not rolled back if DB save fails (orphaned files) |
| H9 | product.controller.ts:57,105 |
DB save happens BEFORE local file save (non-atomic) |
| H10 | product.validation.ts:13 |
price field is .optional() but entity column is required (no default) |
| H11 | products-page.jsx:463-505 |
Sort only sorts current page slice, not entire filtered dataset |
| H12 | product-form.jsx:30-40 |
Category dropdown uses hardcoded mock options instead of API |
| H13 | product-form.jsx:154-156 |
Frontend sends removedImageIds but backend doesn't handle it |
| H15 | product-form.jsx:511 |
Image preview uses /api/images/${filename} — endpoint doesn't exist |
| — | Moved to L17 (downgraded — style inconsistency, no functional impact) | |
| H17 | products-page.jsx:372-378 |
Category filter options hardcoded (5 static strings) |
| H18 | products-page.jsx:310-342 |
Analytics data calculated from mock data; change percentages hardcoded |
| H19 | product_category.validation.ts:31 |
Export named BlogCategoryValidation instead of ProductCategoryValidation |
Medium (22)¶
| # | Location | Issue |
|---|---|---|
| M1 | Product.ts:30-38 |
categoryId FK is nullable — products can exist without category |
| M2 | Product.ts:41-43 |
No DB constraint for price >= 0 (only Zod validation) |
| M3 | ProductCategory.ts:13 |
No explicit @Entity("product_categories") — relies on TypeORM default |
| M4 | ProductCategory.ts:18-20 |
No max length on category name column |
| M5 | ProductCategory.ts:28-32 |
Timestamps use "timestamp" not "timestamptz" (inconsistent with Product entity) |
| M6 | product.service.ts:39 |
Price range filter has no validation minPrice <= maxPrice |
| M7 | product.service.ts:116 |
Object merge {...product, ...payload} has no explicit ID safety |
| M8 | product.controller.ts:60-71 |
Promise.all for file saves has no error handling |
| M9 | product.validation.ts:12 |
categoryId allows 0 or negative (no minimum check) |
| M10 | product_category.validation.ts:31 |
Export named BlogCategoryValidation instead of ProductCategoryValidation — copy-paste naming artifact only; validated fields (name, description) are correct for ProductCategory |
| M11 | product_category.service.ts:68,72 |
createCategory uses toLocaleLowerCase() for duplicate check (line 68) but toLowerCase() for name storage (line 72) — locale-sensitive characters could bypass uniqueness check |
| M12 | product_category.routes.ts:28-31 |
DELETE route has no validateRequest middleware |
| M13 | image.service.ts:7-8 |
Returns undefined if no file provided; controller doesn't handle |
| M14 | product-categories-page.jsx:8 |
Hook's error state not destructured (errors silently ignored) |
| M15 | product-form.jsx:94 |
categoryId stored as string; backend expects number (z.number().int()) |
| M16 | product-form.jsx:218-222 |
Max 10 images check is commented out |
| M17 | product-form.jsx:139 |
Price type mismatch between form (string) and backend (number) |
| M18 | [id]/page.js:22-38 |
Mock product has different image structure than backend entity |
| M19 | products-page.jsx:429-434 |
Price filter uses parseFloat on form strings — NaN if non-numeric |
| M20 | product-performance.jsx:41 |
Typo: "Minecraf App" (missing 't') |
| M21 | product-performance.jsx:27 |
Grid uses grid-cols-5 with no responsive breakpoints |
| M22 | product-form.jsx:441-450 |
Frontend requires description (min 10 chars); backend makes it optional |
Low (19)¶
| # | Location | Issue |
|---|---|---|
| L1 | Product.ts:48-49 |
No ISO 4217 validation for currency codes |
| L2 | Product.ts:51-52 |
Redundant isDeleted boolean alongside @DeleteDateColumn |
| L3 | ProductCategory.ts:34-35 |
Missing isDeleted boolean (inconsistent with Product pattern) |
| L4 | product.service.ts:81 |
baseUrl parameter unused in transformImageUrl call |
| L5 | product.routes.ts:20 |
Redundant router.use(auth(), contextMiddleware) (already applied line 12) |
| L6 | product.validation.ts:23 |
No ISO 4217 validation for currency field |
| L7 | image.controller.ts:6 |
Unsafe type cast req.file as Express.Multer.File (no null check) |
| L8 | product_category.controller.ts:20 |
Inconsistent response structure (nested categories + meta) |
| L9 | products/page.js |
No Next.js metadata export (missing page title/description) |
| L10 | categories/page.js |
No Next.js metadata export |
| L11 | products-page.jsx:799 |
Description truncated with hardcoded slice(0, 40) |
| L12 | products-page.jsx:743 |
Indeterminate checkbox state not semantic (missing ARIA attribute) |
| L13 | product-form.jsx:206 |
File type validation only via MIME (can be spoofed) |
| L14 | product-form.jsx:210 |
5MB file size limit hardcoded (no config) |
| L15 | product-performance.jsx:20-23 |
Date selector non-functional (no click handler) |
| L16 | All files | Zero test coverage across entire Product Catalog system |
| L17 | product-categories-page.jsx:11,14,18 |
API URLs use relative paths (no NEXT_PUBLIC_API_URL prefix) while hook uses absolute — style inconsistency only, both resolve correctly (axios ignores baseURL for absolute URLs) |
| L18 | Product.ts:60 |
@DeleteDateColumn({ nullable: true }) has no explicit type: "timestamptz" — inconsistent with createdAt/updatedAt which specify { type: "timestamptz" }. TypeORM defaults to timestamp (no timezone) |
| L19 | product.routes.ts:28-31 |
Product DELETE route missing validateRequest middleware (same issue as M12 for category DELETE) — :id param has no schema validation |
Complete File Inventory¶
Backend Files (13 files, ~825 LOC)¶
saas-boilerplate/src/entity/Product.ts (62 LOC)¶
Purpose: TypeORM entity defining the products table with columns for product data, pricing, images, and category FK.
What Future Contributors Must Know: This entity uses a DUAL soft-delete pattern (isDeleted boolean + @DeleteDateColumn) but the service layer ONLY checks isDeleted — products deleted via TypeORM's softDelete() or softRemove() will still appear in queries. The images column stores an array of URL/path strings (TEXT array type), not file metadata objects.
Exports:
- Product (class) — TypeORM entity with 10 columns + 1 relation
Columns:
| Column | Type | Constraints | Notes |
|--------|------|-------------|-------|
| id | int (PK) | auto-increment | Standard |
| name | varchar(100) | indexed, not null | Matches validation max |
| images | text[] | nullable | Array of URL strings; no individual URL validation |
| description | text | nullable | No length constraints |
| categoryId | int (FK) | nullable, indexed | FK to ProductCategory; CASCADE delete |
| price | decimal(10,2) | indexed, not null | No DB >= 0 constraint |
| discountPrice | decimal(10,2) | nullable | No constraint < price |
| currency | varchar(3) | default "USD" | No ISO 4217 validation |
| isDeleted | boolean | default false | Redundant with deletedAt |
| createdAt | timestamptz | auto | Timezone-aware |
| updatedAt | timestamptz | auto | Timezone-aware |
| deletedAt | timestamp (no timezone — TypeORM default, no explicit type specified) | auto (@DeleteDateColumn) | Never used by service; inconsistent with createdAt/updatedAt which use timestamptz |
Relations:
- @ManyToOne(() => ProductCategory, { onDelete: "CASCADE" }) — Deleting a category cascades to delete all its products
Dependencies:
- typeorm — Entity, Column, PrimaryGeneratedColumn, ManyToOne, DeleteDateColumn decorators
- ./ProductCategory — relation target
Used By:
- product.service.ts — all CRUD operations
- product_category.service.ts — relation reference
- dbConfig.ts — auto-loaded via glob src/entity/**/*.{ts,js}
Issues: C4 (dual soft-delete bypass), H6 (no image validation), H7 (no discountPrice constraint), M1 (nullable FK), M2 (no price constraint), L1 (no ISO 4217), L2 (redundant isDeleted), L18 (deletedAt missing timestamptz)
saas-boilerplate/src/entity/ProductCategory.ts (36 LOC)¶
Purpose: TypeORM entity defining product categories with name, description, and one-to-many relation to Product.
What Future Contributors Must Know: Despite having @DeleteDateColumn, the service uses repo.remove() (hard delete), not repo.softRemove(). Timestamps use "timestamp" (no timezone) while Product uses "timestamptz" — potential timezone comparison bugs.
Exports:
- ProductCategory (class) — TypeORM entity with 5 columns + 1 relation
Columns:
| Column | Type | Constraints | Notes |
|--------|------|-------------|-------|
| id | int (PK) | auto-increment | Standard |
| name | varchar | indexed, not null | No max length defined |
| description | text | nullable | No constraints |
| createdAt | timestamp | auto | Missing timezone (inconsistent) |
| updatedAt | timestamp | auto | Missing timezone |
| deletedAt | timestamp | auto (@DeleteDateColumn) | Never used — service hard-deletes |
Relations:
- @OneToMany(() => Product, product => product.category) — inverse of Product.category
Dependencies:
- typeorm — decorators
- ./Product — relation target
Used By:
- product_category.service.ts, product.service.ts (category validation)
Issues: C7 (hard delete despite @DeleteDateColumn), M3 (no explicit table name), M4 (no name length), M5 (timezone mismatch), L3 (missing isDeleted)
saas-boilerplate/src/app/modules/v1/Product/product.service.ts (139 LOC)¶
Purpose: Business logic for Product CRUD — list (paginated, filterable), get by ID with similar products, create with permission check, update (permission commented out), soft-delete (no permission check).
What Future Contributors Must Know: Only createProduct has an active permission check ("products.create"). The updateProduct permission check is commented out (line 103), and deleteProductById has none. All queries filter by isDeleted: false but ignore the deletedAt column entirely, creating a soft-delete bypass.
Exports:
- getProductList(query, baseUrl) — paginated list with filters (category, price range, search)
- getProductById(id, baseUrl) — single product + 4 similar products from same category
- createProduct(payload) — creates product with category validation; checks "products.create" permission
- updateProduct(id, payload) — updates product; permission check commented out
- deleteProductById(id) — soft-deletes via isDeleted = true; no permission check
Permission Strings Used:
- "products.create" (line 91) — plural, dot-notation
- "product.update" (line 103, commented out) — singular, dot-notation
- None for delete
Dependencies:
- ../../shared/getDbRepository — database access
- ../../shared/sendResponse — response formatting
- ../../helper/checkPermissionAndThrow — RBAC enforcement
- ../../shared/getPaginationParams — pagination parsing
- ../../shared/getQueryBuilder — TypeORM query builder factory
- ../../helper/transformImageUrl — image URL transformation
- ../../entity/Product, ../../entity/ProductCategory
Used By:
- product.controller.ts — all 5 controller handlers
Side Effects:
- Database reads/writes via TypeORM
- Permission check via checkPermissionAndThrow (reads from permission cache/DB)
Issues: C2 (update perm commented out), C3 (delete no perm), C4 (ignores deletedAt), C18 (plural permission string), H10 (price optional), M6 (no min<=max validation), M7 (unsafe merge), L4 (unused baseUrl)
saas-boilerplate/src/app/modules/v1/Product/product.controller.ts (154 LOC)¶
Purpose: Express request handlers for Product endpoints — handles multipart file uploads (multer), delegates to service, manages image storage flow (cloud vs local).
What Future Contributors Must Know: The image upload flow has a critical ordering bug: the database save happens BEFORE local file saves. If saveBufferToFile fails after the DB record is created, you get a product with invalid image paths. Cloud uploads are also not rolled back on DB failure (orphaned files). The updateProduct handler returns HTTP 201 with message "Product created successfully" — both wrong.
Exports:
- getProductList(req, res) — GET /products
- getProductById(req, res) — GET /products/:id
- createProduct(req, res) — POST /products (multipart)
- updateProduct(req, res) — PATCH /products/:id (multipart)
- deleteProduct(req, res) — DELETE /products/:id
Image Upload Flow:
1. multer parses files (up to 10)
2. If cloud storage: uploadImage() → assigns URLs to payload.images
3. If local storage: map files to /uploads/products/{timestamp}-{name}
4. ProductService.createProduct(payload) — DB SAVE
5. If local storage: saveBufferToFile() for each file — FILE SAVE (after DB!)
Dependencies:
- ../Product/product.service — business logic
- ../../helper/uploadImage — cloud upload
- ../../helper/saveBufferToFile — local file save
- ../../shared/sendResponse — response
- ../../helper/ApiError — error handling
- http-status — status codes
Used By:
- product.routes.ts — route binding
Issues: C16 (wrong status/message on update), H8 (no cloud rollback), H9 (DB before files), M8 (no file save error handling)
saas-boilerplate/src/app/modules/v1/Product/product.routes.ts (36 LOC)¶
Purpose: Express router defining Product endpoints with auth middleware and multer file upload configuration.
What Future Contributors Must Know: GET routes (/ and /:id) are PUBLIC — they appear before the router.use(auth(), contextMiddleware) middleware. POST/PATCH/DELETE require authentication. There's a redundant second router.use(auth(), contextMiddleware) on line 20. No permission middleware exists at the route level — permissions are checked in the service layer only.
Route Table:
| Method | Path | Auth | Middleware | Handler |
|---|---|---|---|---|
| GET | / |
No | — | getProductList |
| GET | /:id |
No | — | getProductById |
| POST | / |
Yes | upload.array("images", 10), validateRequest | createProduct |
| PATCH | /:id |
Yes | upload.array("images", 10), validateRequest | updateProduct |
| DELETE | /:id |
Yes | — | deleteProduct |
Dependencies:
- express.Router
- ../../middlewares/auth — JWT authentication
- ../../middlewares/contextMiddleware — request context (AsyncLocalStorage)
- ../../middlewares/validateRequest — Zod schema validation
- multer — file upload parsing
- ./product.controller
- ./product.validation
Used By:
- routes/v1/index.ts — mounted at /products
Issues: L5 (redundant middleware), L19 (DELETE missing validateRequest)
saas-boilerplate/src/app/modules/v1/Product/product.validation.ts (40 LOC)¶
Purpose: Zod validation schemas for Product create/update requests.
What Future Contributors Must Know: The schema has a CRITICAL dual field mismatch: it defines originalPrice as z.string() but the Product entity has discountPrice as decimal(10,2) — both the name AND type are wrong. Zod's z.object() strips unknown keys by default, so even sending the correct field name discountPrice from the frontend would be silently stripped by validateRequest before reaching the controller. The images field validates as z.string().url() which rejects local file paths like /uploads/products/... — local storage mode will always fail validation. The price field is marked optional but the entity requires it.
Exports:
- ProductValidation.createProduct — Zod schema wrapping productSchema in body
- ProductValidation.updateProduct — Zod schema with .partial() on productSchema
Schema Fields:
| Field | Zod Type | Entity Column | Match? |
|---|---|---|---|
name |
string().min(1).max(100) |
varchar(100) |
Yes |
images |
array(string().url()).optional() |
text[] |
Partial — rejects local paths |
description |
string().optional() |
text nullable |
Yes |
categoryId |
number().int() |
int FK nullable |
Yes |
price |
number().nonnegative().optional() |
decimal(10,2) NOT NULL |
No — optional vs required |
originalPrice |
string().regex(...) |
NO MATCH | No — entity has discountPrice as decimal(10,2) (wrong name AND wrong type: string vs number) |
currency |
string().length(3) |
varchar(3) default "USD" |
Yes |
Issues: C1 (dual originalPrice name+type mismatch + Zod strip), C17 (.url() rejects local paths), H10 (price optional), M9 (categoryId allows <=0)
saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.service.ts (138 LOC)¶
Purpose: Business logic for ProductCategory CRUD — list all, paginated list, get by ID, create with uniqueness check, update with uniqueness check, delete (hard delete).
What Future Contributors Must Know: This file was copy-pasted from BlogCategory service. Line 2 imports BlogCategory from the wrong entity module solely for the wrong type cast to DeepPartial<BlogCategory> on line 104. Uses repo.remove() for deletion which hard-deletes despite the entity having @DeleteDateColumn. Category names are lowercased and trimmed before storage, but createCategory uses toLocaleLowerCase() (line 68) for the duplicate check while using toLowerCase() (line 72) for storage — a locale-sensitive inconsistency. All mutation operations check permissions (product-category.create/update/delete) but these permissions are NOT seeded in the database.
Exports:
- getAllCategories() — returns all categories (no pagination, no permission check)
- categoryList(query) — paginated list (no permission check)
- getCategoryById(id) — single category (no permission check)
- createCategory(payload) — creates with name uniqueness check; requires "product-category.create"
- updateCategory(id, payload) — updates with uniqueness check; requires "product-category.update"
- deleteCategory(id) — HARD DELETE via repo.remove(); requires "product-category.delete"
Permission Strings Used:
- "product-category.create" (line 65)
- "product-category.update" (line 81)
- "product-category.delete" (line 116)
Issues: C5 (wrong validation import in routes), C6 (BlogCategory import + type cast), C7 (hard delete), C8 (permissions not seeded), H3 (no read permissions), M11 (toLocaleLowerCase vs toLowerCase)
saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.controller.ts (97 LOC)¶
Purpose: Express request handlers for ProductCategory CRUD.
What Future Contributors Must Know: Every response message says "Blog" instead of "Product" (5 instances). This is a copy-paste artifact from BlogCategory that was never updated.
Exports:
- getAllCategories, getCategoryList, getCategoryById, createProductCategory, updateProductCategory, deleteProductCategory
Issues: H2 (5 wrong "Blog" messages)
saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.routes.ts (35 LOC)¶
Purpose: Express router for ProductCategory endpoints.
What Future Contributors Must Know: ALL routes require authentication (including GET). The validation middleware imports BlogCategoryValidation instead of ProductCategoryValidation. The DELETE route has no validateRequest middleware.
Route Table:
| Method | Path | Auth | Middleware | Handler |
|---|---|---|---|---|
| GET | /all |
Yes | — | getAllCategories |
| GET | / |
Yes | — | getCategoryList |
| GET | /:id |
Yes | — | getCategoryById |
| POST | / |
Yes | validateRequest(BlogCategoryValidation) | createProductCategory |
| PATCH | /:id |
Yes | validateRequest(BlogCategoryValidation) | updateProductCategory |
| DELETE | /:id |
Yes | — | deleteProductCategory |
Issues: C5 (wrong validation import), H1 (no public read), M12 (no DELETE validation)
saas-boilerplate/src/app/modules/v1/ProductCategory/product_category.validation.ts (36 LOC)¶
Purpose: Zod schemas for ProductCategory validation — but exported as BlogCategoryValidation.
What Future Contributors Must Know: The export name is BlogCategoryValidation, not ProductCategoryValidation. This is a copy-paste naming artifact only — the actual validated fields (name, description) are correct for ProductCategory. No Blog-specific fields exist in the schema.
Issues: H19 (wrong export name), M10 (copy-paste naming artifact)
saas-boilerplate/src/app/modules/v1/Image/image.service.ts (21 LOC)¶
Purpose: Single-file image upload via handleFileUpload helper.
What Future Contributors Must Know: Storage provider is hardcoded to "cloudinary" (line 11) — ignores whatever AppConfig specifies. Returns undefined if no file provided, and the controller doesn't handle this case.
Exports:
- GetImgURL(file) — uploads single file to Cloudinary, returns URL
Issues: H5 (hardcoded cloudinary), M13 (undefined return)
saas-boilerplate/src/app/modules/v1/Image/image.controller.ts (22 LOC)¶
Purpose: Express handler for image upload endpoint.
Issues: L7 (unsafe type cast)
saas-boilerplate/src/app/modules/v1/Image/image.route.ts (14 LOC)¶
Purpose: Express router mounting POST /image with multer single-file upload.
What Future Contributors Must Know: This route has NO authentication middleware. Anyone can upload images without logging in. No rate limiting.
Route Table:
| Method | Path | Auth | Middleware | Handler |
|---|---|---|---|---|
| POST | /image |
No | multer.single("image") | GetImgURL |
Registered at: /api/v1/uploads/image (via routes/v1/index.ts)
Issues: H4 (unauthenticated upload)
Frontend Files (10 files, ~1,961 LOC)¶
Sass-boilerplate-frontend-v1/src/api/products/index.js (0 LOC)¶
Purpose: Intended barrel export for Product API hooks.
What Future Contributors Must Know: This file is completely empty. There are zero React Query hooks for any product CRUD operation. All product data fetching in the frontend uses either hardcoded mock data or raw axios calls. This is the primary blocker for frontend-backend integration.
Issues: C15 (empty — no API hooks)
Sass-boilerplate-frontend-v1/src/api/products/product-categories.js (37 LOC)¶
Purpose: Custom hook useProductCategories() for fetching product categories via raw axios.
What Future Contributors Must Know: This is the ONLY real API integration in the entire Product Catalog frontend. However, it uses raw axios + useState/useEffect instead of React Query (inconsistent with the rest of the codebase which uses React Query v5). The refetch mechanism uses a boolean toggle state, not React Query's built-in refetch.
Exports:
- useProductCategories() — returns { categories, loading, error, setRefetch }
API Call: GET ${NEXT_PUBLIC_API_URL}/product-categories?limit=1000
Issues: Uses raw axios instead of React Query (inconsistent with codebase pattern)
Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/page.js (8 LOC)¶
Purpose: Next.js route page — renders ProductsPage component.
Issues: L9 (no metadata export)
Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/categories/page.js (8 LOC)¶
Purpose: Next.js route page — renders ProductCategoriesPage component.
Issues: L10 (no metadata export)
Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/create/page.js (51 LOC)¶
Purpose: Create product page with ProductForm in create mode.
What Future Contributors Must Know: The entire onSubmit handler is mocked. The real API call (POST to /api/products) is commented out (lines 13-25). Instead, a 1000ms setTimeout simulates the delay, then shows a success toast and redirects. The form data is completely discarded.
Issues: C10 (API call commented out)
Sass-boilerplate-frontend-v1/src/app/(dashboard-layout)/dashboard/products/[id]/page.js (173 LOC)¶
Purpose: Edit product page — fetches product by ID and renders ProductForm in edit mode.
What Future Contributors Must Know: Both the fetch (GET) and save (PATCH) API calls are commented out. The page always loads the same mock product ("Wireless Bluetooth Headphones", price $12.00, 2 placeholder images) regardless of the productId parameter. The mock data has a different image structure (objects with id, url, filename) than the backend entity (array of path strings).
Mock Product Shape:
{
id: productId,
name: "Wireless Bluetooth Headphones",
categoryId: "1", // String, backend expects number
price: "12.00", // String, backend expects number
discountPrice: "10.00", // String
images: [
{ id: "img1", url: "https://via.placeholder.com/...", filename: "headphones-1.jpg" }
]
}
Issues: C11 (mock fetch), C12 (mock save), M18 (image structure mismatch)
Sass-boilerplate-frontend-v1/src/components/pages/products/products-page.jsx (~891 LOC)¶
Purpose: Main products dashboard — table with search, filters, sorting, pagination, analytics cards, bulk operations, RBAC permission gates.
What Future Contributors Must Know: This is a fully-built UI with zero backend integration. The generateProductsData() function returns 15 hardcoded products. All CRUD operations (delete, bulk delete) only modify local React state. The analytics cards show calculated values from mock data with hardcoded change percentages ("+12%", "+18%"). Permission wrappers use product.view/create/update/delete strings that are not seeded in the backend.
State Management: 11 useState hooks — all local, no Redux/React Query/Context.
Permission Strings Referenced:
- product.view (wraps entire page)
- product.create (wraps "Add New Product" button)
- product.update (wraps edit button per row)
- product.delete (wraps delete button per row)
Mock Data: 15 products across 2 categories (Electronics, Cosmetics) with realistic names, descriptions, prices, and timestamps.
TODOs Found:
- Line 391: // TODO: Replace with actual API call
Issues: C9 (all mock data), C13 (local-only delete), H11 (broken sort), H17 (hardcoded categories), H18 (fake analytics), M19 (NaN on non-numeric filter)
Sass-boilerplate-frontend-v1/src/components/pages/products/product-categories-page.jsx (37 LOC)¶
Purpose: Category management page using CustomizeManager component with actual API calls.
What Future Contributors Must Know: This is the ONLY product page with real backend integration. Uses useProductCategories hook for fetching and raw axios for mutations. The CustomizeManager component handles the UI for add/edit/delete. However, API URLs in the mutation functions are missing the NEXT_PUBLIC_API_URL prefix (the axios interceptor base URL covers this, but it's inconsistent with the hook).
Issues: L17 (URL style inconsistency — no functional impact), M14 (error state ignored)
Sass-boilerplate-frontend-v1/src/components/pages/products/product-form.jsx (~535 LOC)¶
Purpose: Reusable form for creating and editing products — React Hook Form with image upload, discount calculation, FormData serialization.
What Future Contributors Must Know: The form is fully built with validation, image handling, and FormData construction — but the onSubmit handler from parent pages is always mocked. Categories are hardcoded (8 options). The removedImageIds feature (for removing existing images on edit) is implemented in the frontend but the backend updateProduct controller doesn't process it. Image preview uses a non-existent /api/images/ endpoint.
Frontend vs Backend Validation Mismatches:
| Field | Frontend | Backend | Mismatch |
|---|---|---|---|
| name | required, 2-100 chars | required, 1-100 chars | Min length differs |
| description | required, 10-1000 chars | optional, no limit | Required vs optional |
| price | required, 0.01-999999.99 | optional, non-negative | Required vs optional |
| categoryId | string | number (int) | Type mismatch |
| discountPrice | optional, < price | originalPrice (WRONG NAME) |
Field name mismatch |
TODOs Found:
- Line 13: // TODO: Replace with actual API call when backend is ready
- Line 30: "Mock categories data - replace with actual API call"
- Line 83: // TODO: Replace with actual API call when backend is ready
Issues: C1 (dual field mismatch via parent), H12 (mock categories), H13 (removedImageIds unsupported), H15 (non-existent endpoint), M15 (type mismatch), M16 (image limit disabled), M17 (price type mismatch), M22 (required vs optional)
Sass-boilerplate-frontend-v1/src/components/pages/dashboard/product-performance.jsx (~217 LOC)¶
Purpose: Dashboard widget intended to show product performance metrics.
What Future Contributors Must Know: This component has NOTHING to do with the Product Catalog. It displays hardcoded project management data — "Minecraf App" (typo), "Web App Project", "Modernize Dashboard", "Dashboard Co" with team member names, budget, and progress bars. It should be renamed to ProjectPerformance or rewritten to display actual product sales metrics.
Issues: C14 (wrong data type), M20 (typo "Minecraf"), M21 (no responsive grid), L15 (non-functional date selector)
Contributor Checklist¶
Risks & Gotchas¶
- The entire frontend Product CRUD is mocked — DO NOT assume any product page works in production
- Permission strings are NOT seeded — adding them requires a seed migration
originalPricein validation schema silently loses discount data — fix the field name todiscountPricefirst- Image validation rejects local paths — if using local storage, the
.url()Zod check must be removed or made conditional - ProductCategory uses hard delete — data is permanently lost; no restore capability
- BlogCategoryValidation is imported instead of ProductCategoryValidation — changing validation will change behavior
Pre-change Verification Steps¶
- Check if product permissions exist in DB:
SELECT * FROM permission WHERE route LIKE '%product%' - Verify image storage config: check
IMAGE_STORAGEenv var and AppConfig - Test with non-super_admin user to confirm permission behavior
- Check if
deletedAtcolumn is populated for any products (indicates TypeORM soft-delete was used somewhere)
Suggested Tests Before PR¶
- Create product with all fields including images → verify DB record and file storage
- Create product with local storage → verify Zod validation passes for file paths
- Update product with commented-out permission → verify access control
- Delete product → verify only
isDeletedflag changes - Create category → verify no Blog fields accepted
- Delete category → verify CASCADE doesn't destroy products unintentionally
Architecture & Design Patterns¶
Code Organization¶
Standard module pattern per feature:
modules/v1/{Feature}/
├── {feature}.service.ts — Business logic + DB operations
├── {feature}.controller.ts — HTTP request handling
├── {feature}.routes.ts — Express router definition
└── {feature}.validation.ts — Zod schema definitions
Design Patterns¶
- Repository Pattern: Via
getDbRepository()shared utility (wraps TypeORM's DataSource) - Service Layer Pattern: Controllers delegate to services; services own business logic
- Middleware Chain:
auth() → contextMiddleware → upload → validateRequest → controller - Factory Pattern: Image upload uses
handleFileUploadwhich dispatches to storage provider - Soft Delete: Dual mechanism (manual
isDeletedboolean + TypeORM@DeleteDateColumn)
State Management Strategy¶
- Backend: Stateless — all state in PostgreSQL via TypeORM
- Frontend (Products): 100% local
useState— no Redux, no React Query, no Context - Frontend (Categories): Raw axios +
useState/useEffectcustom hook — no React Query
Error Handling Philosophy¶
- Backend:
catchAsyncwrapper on all controllers;ApiErrorclass for typed errors; global error handler - Frontend: Minimal — mock data never fails;
product-categories-pagehas no try-catch on mutations
Testing Strategy¶
- Current: 0% coverage. Zero test files across all 23 files.
- Backend: No unit tests, no integration tests
- Frontend: No component tests, no E2E tests
Data Flow¶
Product Create Flow (If Connected)¶
[Frontend Form] → FormData (multipart)
→ [multer middleware] parses files
→ [validateRequest] Zod validation
← FAILS for local paths (.url() check)
← STRIPS unknown keys (discountPrice removed if sent; only originalPrice defined)
← req.body OVERWRITTEN with stripped result (validateRequest.ts:53-56)
→ [product.controller.ts]
→ uploadImage() (cloud) OR map to local paths
→ ProductService.createProduct(payload)
→ checkPermissionAndThrow("products.create") ← FAILS (not seeded)
→ categoryRepo.findOneBy({ id }) ← validates category exists
→ productRepo.create(payload) ← originalPrice ignored (wrong name + wrong type: string vs decimal)
→ productRepo.save(product) ← DB insert (discountPrice always NULL)
→ saveBufferToFile() (local) ← AFTER DB save (non-atomic)
→ sendResponse(201)
Data Entry Points¶
- GET /api/v1/products — Public, no auth, paginated product list
- GET /api/v1/products/:id — Public, no auth, single product + similar products
- POST /api/v1/products — Auth required, multipart form with images
- PATCH /api/v1/products/:id — Auth required, multipart form with images
- DELETE /api/v1/products/:id — Auth required
- GET /api/v1/product-categories/all — Auth required, all categories
- GET /api/v1/product-categories — Auth required, paginated categories
- POST /api/v1/product-categories — Auth required
- PATCH /api/v1/product-categories/:id — Auth required
- DELETE /api/v1/product-categories/:id — Auth required
- POST /api/v1/uploads/image — No auth, single image upload
Data Transformations¶
- Image URLs:
transformImageUrl()prepends base URL to relative paths - Category names: Lowercased and trimmed before storage
- Pagination:
getPaginationParams()extracts page/limit from query - Product filters: Price range parsed from strings; category name matched via join
Data Exit Points¶
- JSON responses via
sendResponse()wrapper - Image files stored to Cloudinary, S3, or local
uploads/products/directory
Integration Points¶
APIs Consumed (Frontend → Backend)¶
| Endpoint | Method | Auth | Frontend Status |
|---|---|---|---|
/products |
GET | No | NOT CALLED (mock data) |
/products |
POST | Yes | NOT CALLED (mock delay) |
/products/:id |
GET | No | NOT CALLED (mock data) |
/products/:id |
PATCH | Yes | NOT CALLED (mock delay) |
/products/:id |
DELETE | Yes | NOT CALLED (local state) |
/product-categories |
GET | Yes | CALLED via useProductCategories |
/product-categories |
POST | Yes | CALLED via axios |
/product-categories/:id |
PATCH | Yes | CALLED via axios |
/product-categories/:id |
DELETE | Yes | CALLED via axios |
/uploads/image |
POST | No | NOT CALLED |
Shared Dependencies (Backend)¶
getDbRepository— DB access (all service files)checkPermissionAndThrow— RBAC (product.service, product_category.service)catchAsync— error wrapping (all controllers)sendResponse— response formatting (all controllers)validateRequest— Zod middleware (product.routes, product_category.routes)handleFileUpload— file upload dispatch (image.service)saveBufferToFile— local file storage (product.controller)uploadImage— cloud upload (product.controller)transformImageUrl— URL transformation (product.service)
Database Access¶
| Entity | Table | Operations | Indexes |
|---|---|---|---|
Product |
products |
CRUD + filter + pagination | name, categoryId, price |
ProductCategory |
product_category |
CRUD + uniqueness check | name |
Events¶
- None — Product Catalog has no event system integration
Cron Jobs¶
- None — no scheduled tasks for products
Dependency Graph¶
Entry Points (routes):
product.routes.ts ──→ product.controller.ts ──→ product.service.ts ──→ Product.ts
└─→ ProductCategory.ts
product_category.routes.ts ──→ product_category.controller.ts ──→ product_category.service.ts ──→ ProductCategory.ts
image.route.ts ──→ image.controller.ts ──→ image.service.ts
Shared Dependencies:
product.service.ts ──→ getDbRepository, checkPermissionAndThrow, getPaginationParams, transformImageUrl
product.controller.ts ──→ uploadImage, saveBufferToFile, sendResponse, ApiError
product_category.service.ts ──→ getDbRepository, checkPermissionAndThrow, ApiError
product_category.controller.ts ──→ sendResponse
image.service.ts ──→ handleFileUpload
Frontend:
products/page.js ──→ products-page.jsx ──→ (NO API — mock data)
products/create/page.js ──→ product-form.jsx ──→ (NO API — mock delay)
products/[id]/page.js ──→ product-form.jsx ──→ (NO API — mock delay)
products/categories/page.js ──→ product-categories-page.jsx ──→ useProductCategories() ──→ axios ──→ /product-categories
Entry Points (Not Imported by Others in Scope)¶
product.routes.ts— route definitionproduct_category.routes.ts— route definitionimage.route.ts— route definitionproducts/page.js— Next.js pageproducts/create/page.js— Next.js pageproducts/[id]/page.js— Next.js pageproducts/categories/page.js— Next.js pageproduct-performance.jsx— dashboard widget
Leaf Nodes (Don't Import Others in Scope)¶
Product.ts— entity (imported by services)ProductCategory.ts— entity (imported by services)product.validation.ts— Zod schemas (imported by routes)product_category.validation.ts— Zod schemas (imported by routes)
Circular Dependencies¶
No circular dependencies detected within scope.
Testing Analysis¶
Test Coverage Summary¶
- Statements: 0%
- Branches: 0%
- Functions: 0%
- Lines: 0%
Test Files¶
None. Zero test files exist for any Product Catalog code.
Testing Gaps¶
- No unit tests for any service function
- No integration tests for API endpoints
- No component tests for React components
- No E2E tests for user flows
- Permission check behavior untested
- Image upload flow untested
- Soft-delete behavior untested
- Category cascade delete untested
Related Code & Reuse Opportunities¶
Similar Features Elsewhere¶
- BlogCategory (
modules/v1/BlogCategory/) — Direct template source for ProductCategory. Has bulk delete, restore, permanent delete, trash functionality that ProductCategory is missing. - Blog (
modules/v1/Blog/) — Similar CRUD pattern with ownership-based permissions, image handling, and rich text. Blog hascheckPermissionAndThrowcalls (though commented out) and slug generation. - Package (
modules/v1/package/) — Similar catalog pattern with categories, pricing. Uses"package.create"(singular) permission format.
Reusable Utilities Available¶
- React Query hooks pattern (
api/blogs/index.js) — Use as template for creatingapi/products/index.jshooks withuseQueryanduseMutation - CustomizeManager (
components/pages/dashboard/customization/customize-manager.jsx) — Already used for categories; could potentially be used for simpler product management - DataTable (
components/custom/data-table.jsxoradvance-table.jsx) — Existing table components with sorting/pagination
Patterns to Follow¶
- Blog API hooks: Reference
api/blogs/index.jsfor React Query hook implementation pattern - Blog CRUD controller: Reference
Blog/blog.controller.tsfor correct image handling with rollback - Permission seeding: Reference
seed/data/permission.tsblog section for seeding product permissions
User Flow Analysis¶
Flow 1: View Products List — BROKEN¶
User navigates to /dashboard/products → ProductsPage renders with 15 hardcoded mock products → API is never called → Delete/edit only affect local state
Flow 2: Create Product — BROKEN¶
User clicks "Add New Product" → navigates to /dashboard/products/create → ProductForm renders with mock categories → Form submission discarded (mock delay + toast) → Redirects to list → No product created
Flow 3: Edit Product — BROKEN¶
User clicks edit on product row → navigates to /dashboard/products/{id} → Mock "Wireless Bluetooth Headphones" loaded (ignores actual ID) → Form submission discarded → No product updated
Flow 4: Delete Product — BROKEN¶
User clicks delete → Product removed from local React state only → Page refresh restores all products → Backend never contacted
Flow 5: Manage Categories — PARTIALLY WORKING¶
User navigates to /dashboard/products/categories → useProductCategories() fetches real data from backend → CustomizeManager renders categories → Add/edit/delete make real API calls → BUT: All routes require auth, permissions not seeded (only super_admin works), response messages say "Blog"
Permission Matrix¶
| Action | Frontend Guard | Backend Auth | Backend Permission | Seeded? | Status |
|---|---|---|---|---|---|
| View Products | product.view |
None (public) | None | N/A | Mismatch — frontend gates, backend public |
| Create Product | product.create |
Yes | products.create |
No | Broken — perm check fails |
| Update Product | product.update |
Yes | product.update |
No | Broken — no perm check |
| Delete Product | product.delete |
Yes | None | N/A | Broken — no perm check |
| View Categories | None | Yes | None | N/A | Works (any authenticated) |
| Create Category | None | Yes | product-category.create |
No | Broken — perm check fails |
| Update Category | None | Yes | product-category.update |
No | Broken — perm check fails |
| Delete Category | None | Yes | product-category.delete |
No | Broken — perm check fails |
Note: super_admin role bypasses all permission checks, so categories work for super_admin only.
Implementation Notes¶
Code Quality Observations¶
- ProductCategory module is a low-effort copy-paste from BlogCategory with 5+ unresolved copy bugs
- Frontend Product pages are well-designed UI with zero backend wiring — appears to be a design-first approach
- Backend Product module is more complete than frontend suggests — endpoints work, validation exists, image handling is implemented
- Image upload module is the simplest in the codebase (21 LOC service) but has a security gap (no auth)
product-performance.jsxis a template remnant that was never adapted for actual product metrics
TODOs and Future Work¶
| File | Line | TODO |
|---|---|---|
product-form.jsx |
13 | Replace with actual API call when backend is ready |
product-form.jsx |
30 | Mock categories data - replace with actual API call |
product-form.jsx |
83 | Replace with actual API call when backend is ready |
products-page.jsx |
391 | Replace with actual API call |
create/page.js |
13 | Replace with actual API call when backend is ready |
[id]/page.js |
25 | Replace with actual API call when backend is ready |
[id]/page.js |
83 | Replace with actual API call when backend is ready |
Dead Code¶
| File | Lines | Description | LOC |
|---|---|---|---|
api/products/index.js |
All | Completely empty file | 0 |
products-page.jsx |
19-308 | generateProductsData() mock generator |
~289 |
products-page.jsx |
391-407 | Commented-out API call | ~16 |
create/page.js |
13-25 | Commented-out API call | ~12 |
[id]/page.js |
25-35 | Commented-out fetch call | ~10 |
[id]/page.js |
84-95 | Commented-out save call | ~11 |
product-form.jsx |
162-180 | Commented-out API call | ~18 |
product-form.jsx |
218-222 | Commented-out image limit check | ~4 |
product-performance.jsx |
All | Template remnant — no relation to products | ~217 |
| Total Dead Code | ~577 LOC |
Technical Debt¶
- 7 commented-out API call blocks that need to be wired up
- BlogCategoryValidation copy-paste must be replaced with proper ProductCategoryValidation
- Dual soft-delete mechanism needs consolidation (pick
isDeletedOR@DeleteDateColumn, not both) - Frontend needs migration from raw axios to React Query for consistency
- Permission strings need standardization (singular vs plural:
products.createvsproduct.update) - Image structure mismatch between frontend (objects with id/url) and backend (string array)
product-performance.jsxneeds complete rewrite or removal
Modification Guidance¶
To Wire Up Frontend API Integration¶
- Create React Query hooks in
api/products/index.jsfollowingapi/blogs/index.jspattern - Replace
generateProductsData()withuseQuerycall toGET /products - Replace mock
handleSubmitin create/edit pages withuseMutationcalls - Replace local-state delete with
useMutationcall toDELETE /products/:id - Replace mock categories in ProductForm with
useProductCategories()hook - Fix
categoryIdtype: send as number, not string - Fix field name:
discountPricenotoriginalPrice
To Fix Backend Permission Issues¶
- Add product permissions to
seed/data/permission.ts:{ route: "product.view", description: "View products" }, { route: "product.create", description: "Create products" }, { route: "product.update", description: "Update products" }, { route: "product.delete", description: "Delete products" }, { route: "product-category.view", description: "View product categories" }, { route: "product-category.create", description: "Create product categories" }, { route: "product-category.update", description: "Update product categories" }, { route: "product-category.delete", description: "Delete product categories" }, - Standardize permission strings (pick singular
product.*not pluralproducts.*) - Uncomment
updateProductpermission check in service - Add
deleteProductByIdpermission check in service - Run seed to populate permissions
To Fix Copy-Paste Bugs in ProductCategory¶
- Rename
BlogCategoryValidationexport toProductCategoryValidation - Update import in
product_category.routes.ts - Fix type cast in
product_category.service.ts:104fromBlogCategorytoProductCategory - Update all 5 controller messages from "Blog" to "Product"
- Replace
repo.remove()withrepo.softRemove()in delete function - Remove Blog-specific fields from validation schema if present
Testing Checklist for Changes¶
- Product create with all fields + images succeeds (DB record + files saved)
- Product create with local storage passes Zod validation
- Product update with active permission check enforces RBAC
- Product delete with permission check enforces RBAC
- Product list excludes soft-deleted products (both
isDeletedanddeletedAt) - Category create validates as ProductCategory (not BlogCategory)
- Category delete uses soft delete (not hard delete)
- Image upload requires authentication
-
discountPricefield properly maps from frontend to backend - Frontend permission wrappers match backend permission strings
- Category cascade delete doesn't unexpectedly destroy products
- Frontend delete calls backend API and refreshes list
- Price filter handles edge cases (NaN, min > max)
Generated by document-project workflow (deep-dive mode)
Base Documentation: docs/index.md
Scan Date: 2026-04-11
Analysis Mode: Exhaustive
Files Read: 23 (13 backend + 10 frontend)
Total Issues: 76 (18 Critical, 17 High, 22 Medium, 19 Low)
Dead Code: ~577 LOC across 10 locations