Skip to content

[Refactoring] Services: Extract reusable Prisma query builders from ProductService #272

@syed-reza98

Description

@syed-reza98

Problem

The ProductService class in src/lib/services/product.service.ts is 1,662 lines and contains 30+ methods. While well-organized, it has significant code duplication in Prisma query patterns:

  • Repeated include blocks for product relations (category, brand, variants, _count) appear in 10+ methods
  • Identical relation selection patterns duplicated across CRUD operations
  • Similar error handling patterns repeated throughout
  • Maintenance burden - any change to query structure requires updates in multiple places
  • Testing complexity - large service class is harder to test comprehensively

This makes the service harder to maintain, test, and extend.

Current Code Location

  • File: src/lib/services/product.service.ts
  • Lines: 1,662 total
  • Complexity: High (30+ methods, multiple concerns)
  • Key methods affected: getProducts, getProductById, getProductBySlug, createProduct, updateProduct, restoreProduct, updateInventory, and others

Proposed Refactoring

Extract reusable Prisma query configuration builders into a separate module or class methods to eliminate duplication.

Benefits

  • DRY principle - Single source of truth for query patterns
  • Consistency - All queries use the same relation structure
  • Easier maintenance - Update query patterns in one place
  • Better testability - Query builders can be tested independently
  • Improved readability - Main service methods focus on business logic, not query construction
  • Type safety - Centralized query builders ensure consistent typing

Suggested Approach

  1. Create query builder helpers in the ProductService class:

    • getProductInclude() - Standard product relations
    • getProductSelect() - Common selection patterns
    • getVariantSelect() - Standard variant selection
  2. Extract common patterns:

    • Full product with relations (for detail views)
    • List product with minimal relations (for tables/lists)
    • Variant-only queries (for inventory operations)
  3. Refactor existing methods to use builders:

    • Replace duplicated include blocks with method calls
    • Maintain same functionality while reducing code
  4. Consider extracting to separate module if builders grow large:

    • src/lib/services/product/query-builders.ts
    • Keep service focused on business logic

Code Example

Before (current duplication pattern):

// Appears 10+ times with slight variations
async getProductById(productId: string, storeId: string): Promise(ProductWithRelations | null) {
  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' },
      },
      _count: { select: { orderItems: true, reviews: true } },
    },
  });
  return this.normalizeProductFields(product) as unknown as ProductWithRelations;
}

// Similar block repeated in: getProductBySlug, createProduct, updateProduct, 
// restoreProduct, updateInventory, etc.

After (using query builders):

// Helper methods (add to ProductService class)
private getStandardProductInclude(): Prisma.ProductInclude {
  return {
    category: { select: { id: true, name: true, slug: true } },
    brand: { select: { id: true, name: true, slug: true } },
    variants: {
      select: this.getStandardVariantSelect(),
      orderBy: { isDefault: 'desc' as const },
    },
    _count: { select: { orderItems: true, reviews: true } },
  };
}

private getStandardVariantSelect() {
  return {
    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,
  };
}

private getListProductInclude(): Prisma.ProductInclude {
  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 } },
  };
}

// Refactored methods (cleaner, more maintainable)
async getProductById(productId: string, storeId: string): Promise(ProductWithRelations | null) {
  const product = await prisma.product.findFirst({
    where: { id: productId, storeId, deletedAt: null },
    include: this.getStandardProductInclude(),
  });
  return this.normalizeProductFields(product) as unknown as ProductWithRelations;
}

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: this.getListProductInclude(), // Use list-optimized include
      orderBy,
      take: perPage,
      skip: (page - 1) * perPage,
    }),
    prisma.product.count({ where }),
  ]);

  return {
    products: products.map(p => this.normalizeProductFields(p)) as unknown as ProductWithRelations[],
    pagination: { page, perPage, total, totalPages: Math.ceil(total / perPage) },
  };
}

Impact Assessment

  • Effort: Medium - Requires careful refactoring of 10+ methods (~4-6 hours)
  • Risk: Low-Medium - Query behavior must remain identical (requires thorough testing)
  • Benefit: High - Significantly improves maintainability and reduces code by ~200+ lines
  • Priority: Medium-High - High-impact improvement to a critical service

Related Files

  • src/lib/services/product.service.ts (primary file)
  • src/lib/services/order.service.ts (similar patterns, could benefit from same approach)
  • src/lib/services/inventory.service.ts (may share some query patterns)

Testing Strategy

  1. Preserve existing behavior:

    • All existing tests must pass without modification
    • Query results must be identical to current implementation
  2. Unit test query builders:

    • Test getStandardProductInclude() returns correct structure
    • Test getListProductInclude() returns optimized structure
    • Test variant selection patterns
  3. Integration tests:

    • Verify product list queries return correct data
    • Verify product detail queries include all relations
    • Test all CRUD operations still work correctly
  4. Performance testing:

    • Benchmark query performance before/after
    • Ensure no performance regression
  5. Manual testing:

    • Test product listing pages
    • Test product detail pages
    • Test product creation/update flows
    • Verify inventory management still works

AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring

  • expires on Mar 6, 2026, 1:56 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions