Skip to content

[Refactoring] Services: Extract duplicated Prisma include patterns into reusable query builders #230

@syed-reza98

Description

@syed-reza98

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

  1. 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;
    }
  2. 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);
    }
  3. Apply to all query methods in the service:

    • getProducts() → use getProductListIncludes()
    • getProductById() → use getProductDetailIncludes()
    • getProductBySlug() → use getProductDetailIncludes()
    • getLowStockProducts() → use getProductListIncludes()
    • Performance-critical queries → use getProductMinimalIncludes()
  4. Consider creating a base pattern that other services can follow:

    • order.service.ts, category.service.ts, inventory.service.ts have 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

  1. Verify existing behavior - Run existing tests for product.service.ts
  2. Add unit tests for new query builder methods
  3. Test all affected endpoints:
    • GET /api/products (list view)
    • GET /api/products/:id (detail view)
    • GET /store/:slug (storefront product pages)
  4. Performance testing - Verify query performance hasn't changed
  5. 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

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions