-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
Multiple service files (product.service.ts, order.service.ts, inventory.service.ts, category.service.ts) contain duplicated patterns for:
- Query builder methods:
buildWhereClause(),buildOrderByClause()repeated with similar logic - Multi-tenant validation:
storeIdanddeletedAt: nullchecks scattered throughout - Error handling patterns: Try-catch with generic error messages duplicated across methods
- 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 repeatedAfter (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.tssrc/lib/services/order.service.tssrc/lib/services/inventory.service.tssrc/lib/services/category.service.ts
Component Layer:
src/components/integrations/facebook/dashboard.tsxsrc/components/orders-table.tsxsrc/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
-
Utility Tests: Unit test each utility function
- Test edge cases (empty strings, undefined values)
- Test validation logic
- Test error handling
-
Service Tests: Ensure refactored services work identically
- Compare query outputs before/after refactor
- Test multi-tenant isolation
- Test pagination and sorting
-
Component Tests: Verify API call utilities work correctly
- Mock API responses
- Test loading states
- Test error scenarios
- Test success callbacks
-
Integration Tests: End-to-end tests for critical flows
- Product search and filtering
- Order management
- Facebook integration operations
Migration Strategy
- Phase 1: Create utility files with tests
- Phase 2: Refactor one service (e.g., category.service.ts - smallest)
- Phase 3: Validate and test refactored service
- Phase 4: Apply to remaining services incrementally
- Phase 5: Refactor component API calls
- 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
Type
Projects
Status