Skip to content

[Refactoring] Service Layer: Extract duplicate Prisma include patterns in product.service.ts #232

@syed-reza98

Description

@syed-reza98

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

  1. 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;
  1. 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
}
  1. 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 times

After:

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

  1. Unit Tests: Verify that refactored methods return the same data structure
  2. Integration Tests: Test product listing, detail pages, and search functionality
  3. Visual Regression: Check that product cards and detail pages render correctly
  4. 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.ts utility 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

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