-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Problem
The product.service.ts file (1,662 lines) contains 8 nearly identical Prisma include configurations across multiple methods. This duplication makes the code harder to maintain, increases the risk of inconsistencies when adding new relations, and bloats the file unnecessarily.
Current Code Location
- File:
src/lib/services/product.service.ts(1,662 lines) - Methods affected:
getProducts()(lines 206-234)getProductById()(lines 270-317)getProductBySlug()(lines 340-365)getLowStockProducts()(lines ~395-430)- And 4 more query methods
- Complexity: High - affects core product retrieval logic
Proposed Refactoring
Extract the repeated Prisma include configurations into reusable constants or helper methods that define different "include profiles" based on the use case (list view, detail view, minimal view).
Benefits
- DRY Principle: Eliminates ~200 lines of duplicated code
- Maintainability: Single source of truth for relations - add a new relation in one place
- Consistency: Ensures all queries return the same structure for the same context
- Type Safety: TypeScript can better infer types from centralized configurations
- Performance: Easier to optimize queries by adjusting include profiles in one place
Suggested Approach
- Create Include Profile Constants at the top of the class:
// Product query include profiles
private static readonly PRODUCT_INCLUDES = {
// Minimal - for list views
minimal: {
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,
},
},
},
// Full - for detail views
full: {
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' as const },
},
attributes: {
select: {
id: true,
productId: true,
attributeId: true,
value: true,
attribute: {
select: {
id: true,
name: true,
values: true,
},
},
},
},
_count: {
select: {
orderItems: true,
reviews: true,
},
},
},
} as const;- Refactor methods to use include profiles:
async getProducts(
storeId: string,
filters: ProductSearchFilters = {},
page: number = 1,
perPage: number = 10
): Promise(ProductListResult) {
const where = this.buildWhereClause(storeId, filters);
const orderBy = this.buildOrderByClause(filters.sortBy, filters.sortOrder);
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: ProductService.PRODUCT_INCLUDES.minimal, // ✅ Single source of truth
orderBy,
take: perPage,
skip: (page - 1) * perPage,
}),
prisma.product.count({ where }),
]);
// ... rest of method
}
async getProductById(
productId: string,
storeId: string
): Promise(ProductWithRelations | null) {
const product = await prisma.product.findFirst({
where: {
id: productId,
storeId,
deletedAt: null,
},
include: ProductService.PRODUCT_INCLUDES.full, // ✅ Single source of truth
});
// ... rest of method
}- Add helper method for custom includes (if needed):
/**
* Create a custom include configuration by merging with a base profile
*/
private static getIncludeConfig(
profile: keyof typeof ProductService.PRODUCT_INCLUDES,
customizations?: Partial(typeof ProductService.PRODUCT_INCLUDES.full)
) {
return {
...ProductService.PRODUCT_INCLUDES[profile],
...customizations,
};
}Code Example
Before:
async getProducts(...) {
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
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,
price: true,
inventoryQty: true,
isDefault: true,
image: true,
},
orderBy: { isDefault: 'desc' },
},
_count: {
select: {
orderItems: true,
reviews: true,
},
},
},
orderBy,
take: perPage,
skip: (page - 1) * perPage,
}),
// ... same include repeated 7 more timesAfter:
async getProducts(...) {
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: ProductService.PRODUCT_INCLUDES.minimal,
orderBy,
take: perPage,
skip: (page - 1) * perPage,
}),
// Single source of truth - much cleaner!Impact Assessment
-
Effort: Medium (2-3 hours)
- Extract include patterns into constants
- Refactor 8 methods to use new constants
- Run tests to ensure no regressions
- Update any type definitions if needed
-
Risk: Low
- No logic changes, just restructuring
- Easy to validate with existing tests
- Can be done incrementally (one method at a time)
-
Benefit: High
- Reduces file by ~200 lines
- Makes future schema changes much easier
- Improves code readability significantly
- Prevents inconsistencies between queries
-
Priority: High - Affects core product functionality used throughout the application
Related Files
src/lib/services/product.service.ts(primary file)src/lib/services/category.service.ts(similar pattern could be applied)src/lib/services/inventory.service.ts(similar pattern could be applied)src/lib/services/order.service.ts(similar pattern could be applied)
Testing Strategy
- Unit Tests: Verify that refactored methods return the same data structure
- Integration Tests: Test product listing, detail pages, and search functionality
- Visual Regression: Check that product cards and detail pages render correctly
- Performance: Ensure query performance is unchanged (should be identical)
Additional Notes
- This pattern can be applied to other service files (order.service.ts, inventory.service.ts) for consistency
- Consider creating a shared
query-builder.tsutility if this pattern is useful across multiple services - Document the include profiles with JSDoc comments explaining when to use each profile
AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring
- expires on Feb 28, 2026, 2:12 PM UTC
Reactions are currently unavailable
Metadata
Metadata
Assignees
Type
Projects
Status
Backlog