-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
The product.service.ts file (1662 lines) contains 8+ nearly identical Prisma include blocks that define how to fetch related product data. Each query method (getProducts, getProductById, getProductBySlug, etc.) repeats the same 30-50 lines of include configuration for categories, brands, variants, and counts.
This duplication:
- Makes the codebase harder to maintain (changes must be applied in 8+ places)
- Increases the risk of inconsistencies between queries
- Adds ~400 lines of unnecessary code
- Makes it difficult to add new relations or modify existing ones
Current Code Location
- File:
src/lib/services/product.service.ts(1662 lines) - Affected methods:
getProducts(),getProductById(),getProductBySlug(),getLowStockProducts(), and 4+ other methods - Complexity: High - Each method contains 30-50 lines of duplicated include configuration
Proposed Refactoring
Extract Prisma include configurations into reusable query builder methods that can be composed based on the data requirements of each endpoint.
Benefits
- Reduces code by ~400 lines (24% reduction in file size)
- Single source of truth for product query configurations
- Easier maintenance - Changes to includes happen in one place
- Better testability - Query builders can be tested independently
- Improved consistency - All queries use the same include patterns
- Performance optimization - Easy to create "light" vs "detailed" query variants
Suggested Approach
-
Create query builder helper methods at the class level:
/** * Get standard product includes for list views (lighter payload) */ private getProductListIncludes() { return { category: { select: { id: true, name: true, slug: true } }, brand: { select: { id: true, name: true, slug: true } }, variants: { select: { id: true, name: true, sku: true, price: true, inventoryQty: true, isDefault: true, image: true, }, orderBy: { isDefault: 'desc' as const }, }, _count: { select: { orderItems: true, reviews: true }, }, } as const; } /** * Get detailed product includes for detail views (full data) */ private getProductDetailIncludes() { return { ...this.getProductListIncludes(), variants: { select: { id: true, name: true, sku: true, barcode: true, price: true, compareAtPrice: true, inventoryQty: true, lowStockThreshold: true, weight: true, image: true, options: true, isDefault: true, createdAt: true, updatedAt: true, }, orderBy: { isDefault: 'desc' as const }, }, attributes: { select: { id: true, productId: true, attributeId: true, value: true, attribute: { select: { id: true, name: true, values: true }, }, }, }, } as const; } /** * Get minimal product includes for performance-critical queries */ private getProductMinimalIncludes() { return { variants: { where: { isDefault: true }, select: { id: true, price: true, inventoryQty: true }, }, } as const; }
-
Refactor existing query methods to use the builders:
// BEFORE (repeated in 8+ methods): async getProductById(productId: string, storeId: string) { const product = await prisma.product.findFirst({ where: { id: productId, storeId, deletedAt: null }, include: { category: { select: { id: true, name: true, slug: true } }, brand: { select: { id: true, name: true, slug: true } }, variants: { select: { id: true, name: true, sku: true, barcode: true, price: true, compareAtPrice: true, inventoryQty: true, lowStockThreshold: true, weight: true, image: true, options: true, isDefault: true, createdAt: true, updatedAt: true, }, orderBy: { isDefault: 'desc' }, }, attributes: { select: { id: true, productId: true, attributeId: true, value: true, attribute: { select: { id: true, name: true, values: true }, }, }, }, _count: { select: { orderItems: true, reviews: true }, }, }, }); if (!product) return null; return this.normalizeProductFields(product); } // AFTER (clean and maintainable): async getProductById(productId: string, storeId: string) { const product = await prisma.product.findFirst({ where: { id: productId, storeId, deletedAt: null }, include: this.getProductDetailIncludes(), }); if (!product) return null; return this.normalizeProductFields(product); }
-
Apply to all query methods in the service:
getProducts()→ usegetProductListIncludes()getProductById()→ usegetProductDetailIncludes()getProductBySlug()→ usegetProductDetailIncludes()getLowStockProducts()→ usegetProductListIncludes()- Performance-critical queries → use
getProductMinimalIncludes()
-
Consider creating a base pattern that other services can follow:
order.service.ts,category.service.ts,inventory.service.tshave similar duplication- Create a pattern/guideline for all service files
Impact Assessment
- Effort: Medium (4-6 hours) - Straightforward extraction, needs careful testing
- Risk: Low - Pure refactoring, no logic changes, existing tests will catch issues
- Benefit: High - Significant code reduction, much easier to maintain
- Priority: High - This pattern affects multiple services and can prevent future duplication
Related Files
Files with similar duplication patterns that could benefit from the same approach:
src/lib/services/order.service.ts(863 lines)src/lib/services/inventory.service.ts(1347 lines)src/lib/services/category.service.ts(739 lines)
Testing Strategy
- Verify existing behavior - Run existing tests for product.service.ts
- Add unit tests for new query builder methods
- Test all affected endpoints:
- GET /api/products (list view)
- GET /api/products/:id (detail view)
- GET /store/:slug (storefront product pages)
- Performance testing - Verify query performance hasn't changed
- Manual testing - Test product list, detail, and low stock pages in the UI
Success Metrics
- ✅ File size reduced from 1662 lines to ~1250 lines (~25% reduction)
- ✅ All 8+ query methods use shared include configurations
- ✅ All existing tests pass
- ✅ No performance degradation
- ✅ Pattern documented for other services to follow
AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring
- expires on Feb 28, 2026, 1:41 PM UTC
Metadata
Metadata
Assignees
Type
Projects
Status