Skip to content

[Refactoring] Utilities: Extract common query builder, API call, and validation patterns #234

@syed-reza98

Description

@syed-reza98

Problem

Multiple service files (product.service.ts, order.service.ts, inventory.service.ts, category.service.ts) contain duplicated patterns for:

  1. Query builder methods: buildWhereClause(), buildOrderByClause() repeated with similar logic
  2. Multi-tenant validation: storeId and deletedAt: null checks scattered throughout
  3. Error handling patterns: Try-catch with generic error messages duplicated across methods
  4. API call patterns: Repetitive fetch-toast-error handling across 9+ handler functions in components

These patterns are copy-pasted across multiple files, leading to:

  • Increased maintenance burden (fix in one place, forget others)
  • Inconsistent error messages and validation
  • Difficult to enforce security best practices (e.g., multi-tenant isolation)
  • ~500 lines of duplicated boilerplate code

Current Code Locations

Service Layer Duplication:

  • src/lib/services/product.service.ts (lines ~145-190, ~1420-1450)
  • src/lib/services/order.service.ts (similar patterns)
  • src/lib/services/inventory.service.ts (similar patterns)
  • src/lib/services/category.service.ts (lines ~100-150)

API Call Duplication (Component Layer):

  • src/components/integrations/facebook/dashboard.tsx (9 handler functions with identical structure)
  • src/components/orders-table.tsx (multiple API calls with same pattern)
  • src/components/product-edit-form.tsx (API calls with same error handling)

Proposed Refactoring

Create a shared utilities library with reusable patterns for common operations.

Benefits

  • DRY Principle: Eliminates ~500 lines of duplicated code
  • Consistency: Ensures all services use the same validation and error handling
  • Security: Centralized multi-tenant validation reduces risk of data leakage
  • Maintainability: Update once, applies everywhere
  • Type Safety: Reusable generic functions with proper TypeScript types
  • Testing: Test utilities once, confidence in all usages

Suggested Approach

1. Create Query Builder Utility

Create src/lib/utils/query-builder.ts:

import { Prisma } from '`@prisma/client`';

/**
 * Base where clause builder with multi-tenant isolation
 * Always includes storeId and soft-delete check
 */
export function buildBaseWhere(storeId: string): Prisma.ProductWhereInput {
  return {
    storeId,
    deletedAt: null,
  };
}

/**
 * Add search filters to where clause
 * Supports case-insensitive search across multiple fields
 */
export function addSearchFilter(T extends Record<string, unknown)>(
  where: T,
  searchTerm: string | undefined,
  searchFields: string[]
): T {
  if (!searchTerm) return where;

  return {
    ...where,
    OR: searchFields.map(field => ({
      [field]: { contains: searchTerm, mode: 'insensitive' },
    })),
  } as T;
}

/**
 * Build order by clause with validation
 * Ensures only allowed fields can be sorted
 */
export function buildOrderBy(
  sortBy: string = 'createdAt',
  sortOrder: 'asc' | 'desc' = 'desc',
  allowedFields: string[]
): Record(string, 'asc' | 'desc') {
  const field = allowedFields.includes(sortBy) ? sortBy : allowedFields[0];
  return { [field]: sortOrder };
}

/**
 * Add date range filter to where clause
 */
export function addDateRangeFilter(T extends Record<string, unknown)>(
  where: T,
  dateField: string,
  startDate?: Date,
  endDate?: Date
): T {
  if (!startDate && !endDate) return where;

  const dateFilter: Record(string, unknown) = {};
  if (startDate) dateFilter.gte = startDate;
  if (endDate) dateFilter.lte = endDate;

  return {
    ...where,
    [dateField]: dateFilter,
  } as T;
}

Usage in product.service.ts:

private buildWhereClause(
  storeId: string,
  filters: ProductSearchFilters
): Prisma.ProductWhereInput {
  // Start with base multi-tenant where clause
  let where = buildBaseWhere(storeId);

  // Add search across multiple fields
  where = addSearchFilter(where, filters.search, ['name', 'description', 'sku']);

  // Add date range if provided
  where = addDateRangeFilter(where, 'createdAt', filters.startDate, filters.endDate);

  // Add specific filters
  if (filters.categoryId) {
    where.categoryId = filters.categoryId;
  }

  if (filters.status !== undefined) {
    where.status = filters.status;
  }

  return where;
}

private buildOrderByClause(
  sortBy?: string,
  sortOrder?: 'asc' | 'desc'
): Prisma.ProductOrderByWithRelationInput {
  return buildOrderBy(
    sortBy,
    sortOrder,
    ['name', 'price', 'createdAt', 'updatedAt'] // Allowed sort fields
  );
}

2. Create API Utilities

Create src/lib/utils/api-helpers.ts:

import { toast } from 'sonner';

/**
 * Standard API error response
 */
export interface ApiError {
  error: string;
  details?: unknown;
}

/**
 * Standard API success response
 */
export interface ApiResponse(T = unknown) {
  success: boolean;
  data?: T;
  error?: string;
  message?: string;
}

/**
 * Execute async API call with loading state and error handling
 * Standardizes the pattern: setLoading(true) -> try/catch -> toast -> setLoading(false)
 */
export async function executeApiCall(T)(
  apiCall: () => Promise(Response),
  options: {
    loadingState?: (loading: boolean) => void;
    successMessage?: string | ((data: T) => string);
    errorMessage?: string;
    onSuccess?: (data: T) => void | Promise(void);
    onError?: (error: string) => void;
  } = {}
): Promise(T | null) {
  const {
    loadingState,
    successMessage,
    errorMessage = 'Operation failed',
    onSuccess,
    onError,
  } = options;

  loadingState?.(true);

  try {
    const response = await apiCall();
    const data = await response.json();

    if (data.success || response.ok) {
      // Success handling
      const message = typeof successMessage === 'function'
        ? successMessage(data.data || data)
        : successMessage;

      if (message) {
        toast.success(message);
      }

      if (onSuccess) {
        await onSuccess(data.data || data);
      }

      return data.data || data;
    } else {
      // API returned error
      toast.error(data.error || errorMessage);
      onError?.(data.error || errorMessage);
      return null;
    }
  } catch (error) {
    // Network or parsing error
    console.error('API call failed:', error);
    const errorMsg = error instanceof Error ? error.message : errorMessage;
    toast.error(errorMsg);
    onError?.(errorMsg);
    return null;
  } finally {
    loadingState?.(false);
  }
}

/**
 * Wrapper for POST API calls
 */
export function postApi(url: string, body: unknown): Promise(Response) {
  return fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
}

/**
 * Wrapper for GET API calls
 */
export function getApi(url: string): Promise(Response) {
  return fetch(url, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
  });
}

Usage in FacebookDashboard:

// Before: 20+ lines per handler
const handleConnect = async () => {
  setConnecting(true);
  try {
    const response = await fetch('/api/integrations/facebook/oauth/connect');
    const data = await response.json();

    if (data.url) {
      window.location.href = data.url;
    } else {
      toast.error(data.error || 'Failed to start Facebook connection');
    }
  } catch (error) {
    console.error('Connect error:', error);
    toast.error('Failed to connect to Facebook');
  } finally {
    setConnecting(false);
  }
};

// After: 5 lines using utility
const handleConnect = () => {
  executeApiCall(
    () => getApi('/api/integrations/facebook/oauth/connect'),
    {
      loadingState: setConnecting,
      errorMessage: 'Failed to connect to Facebook',
      onSuccess: (data) => {
        if (data.url) {
          window.location.href = data.url;
        }
      },
    }
  );
};

// Even cleaner with product sync
const handleSync = () => {
  executeApiCall(
    () => postApi('/api/integrations/facebook/products/sync', { syncAll: true }),
    {
      loadingState: setSyncing,
      successMessage: (data) => `Synced ${data.successCount} products successfully!`,
      errorMessage: 'Failed to sync products',
      onSuccess: () => window.location.reload(),
    }
  );
};

3. Create Validation Utilities

Create src/lib/utils/validation-helpers.ts:

/**
 * Validate multi-tenant access
 * Ensures user can only access their store's data
 */
export function validateMultiTenantAccess(
  resourceStoreId: string,
  requestStoreId: string,
  resourceType: string = 'Resource'
): void {
  if (resourceStoreId !== requestStoreId) {
    throw new Error(`${resourceType} not found or access denied`);
  }
}

/**
 * Validate required fields
 */
export function validateRequiredFields(T extends Record<string, unknown)>(
  data: T,
  requiredFields: (keyof T)[]
): void {
  const missing = requiredFields.filter(field => !data[field]);
  
  if (missing.length > 0) {
    throw new Error(`Missing required fields: ${missing.join(', ')}`);
  }
}

/**
 * Sanitize pagination parameters
 */
export function sanitizePagination(
  page?: number,
  perPage?: number,
  maxPerPage: number = 100
): { page: number; perPage: number; skip: number } {
  const sanitizedPage = Math.max(1, page || 1);
  const sanitizedPerPage = Math.min(maxPerPage, Math.max(1, perPage || 10));
  
  return {
    page: sanitizedPage,
    perPage: sanitizedPerPage,
    skip: (sanitizedPage - 1) * sanitizedPerPage,
  };
}

Code Example

Before (Scattered Duplication):

// product.service.ts
private buildWhereClause(storeId: string, filters: ProductSearchFilters) {
  const where: Prisma.ProductWhereInput = {
    storeId,
    deletedAt: null,
  };

  if (filters.search) {
    where.OR = [
      { name: { contains: filters.search, mode: 'insensitive' } },
      { description: { contains: filters.search, mode: 'insensitive' } },
    ];
  }
  // ... 30 more lines
}

// order.service.ts - DUPLICATED PATTERN
private buildWhereClause(storeId: string, filters: OrderSearchFilters) {
  const where: Prisma.OrderWhereInput = {
    storeId,
    deletedAt: null,
  };

  if (filters.search) {
    where.OR = [
      { orderNumber: { contains: filters.search, mode: 'insensitive' } },
      { customerName: { contains: filters.search, mode: 'insensitive' } },
    ];
  }
  // ... 30 more lines (almost identical)
}

// category.service.ts - DUPLICATED AGAIN
// ... same pattern repeated

After (Centralized Utilities):

// product.service.ts
private buildWhereClause(storeId: string, filters: ProductSearchFilters) {
  let where = buildBaseWhere(storeId);
  where = addSearchFilter(where, filters.search, ['name', 'description', 'sku']);
  where = addDateRangeFilter(where, 'createdAt', filters.startDate, filters.endDate);
  
  // Only service-specific filters remain
  if (filters.categoryId) where.categoryId = filters.categoryId;
  if (filters.status) where.status = filters.status;
  
  return where;
}

// order.service.ts - Uses same utilities
private buildWhereClause(storeId: string, filters: OrderSearchFilters) {
  let where = buildBaseWhere(storeId);
  where = addSearchFilter(where, filters.search, ['orderNumber', 'customerName']);
  where = addDateRangeFilter(where, 'createdAt', filters.startDate, filters.endDate);
  
  // Only service-specific filters
  if (filters.status) where.status = filters.status;
  
  return where;
}

Impact Assessment

  • Effort: Medium-High (4-6 hours)

    • Create 3 utility files (~2 hours)
    • Refactor 4 service files (~2 hours)
    • Refactor 3 component files (~1 hour)
    • Testing and validation (~1 hour)
  • Risk: Low-Medium

    • Well-defined patterns being extracted
    • Can be done incrementally (one service at a time)
    • Easy to test utilities in isolation
    • Existing tests catch regressions
  • Benefit: Very High

    • Eliminates ~500 lines of duplicated code
    • Ensures consistency across all services
    • Improves security with centralized validation
    • Makes future changes much easier
    • Reduces cognitive load when reading code
  • Priority: High - Affects core business logic across entire application

Related Files

Service Layer:

  • src/lib/services/product.service.ts
  • src/lib/services/order.service.ts
  • src/lib/services/inventory.service.ts
  • src/lib/services/category.service.ts

Component Layer:

  • src/components/integrations/facebook/dashboard.tsx
  • src/components/orders-table.tsx
  • src/components/product-edit-form.tsx

New Utility Files:

  • src/lib/utils/query-builder.ts (new)
  • src/lib/utils/api-helpers.ts (new)
  • src/lib/utils/validation-helpers.ts (new)

Testing Strategy

  1. Utility Tests: Unit test each utility function

    • Test edge cases (empty strings, undefined values)
    • Test validation logic
    • Test error handling
  2. Service Tests: Ensure refactored services work identically

    • Compare query outputs before/after refactor
    • Test multi-tenant isolation
    • Test pagination and sorting
  3. Component Tests: Verify API call utilities work correctly

    • Mock API responses
    • Test loading states
    • Test error scenarios
    • Test success callbacks
  4. Integration Tests: End-to-end tests for critical flows

    • Product search and filtering
    • Order management
    • Facebook integration operations

Migration Strategy

  1. Phase 1: Create utility files with tests
  2. Phase 2: Refactor one service (e.g., category.service.ts - smallest)
  3. Phase 3: Validate and test refactored service
  4. Phase 4: Apply to remaining services incrementally
  5. Phase 5: Refactor component API calls
  6. Phase 6: Document utilities and patterns in README

This incremental approach minimizes risk and allows validation at each step.

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