wulf-pulse/lib/utils/api-helpers.ts
root 6eee14f8af Add comprehensive admin features and multi-system integration
- Add admin dashboard with sync controls and data browser
- Implement RMM, Auvik, and Addigy organization mappings
- Add chunked ticket sync with progress tracking
- Implement entity sync service with rate limiting
- Add analytics engine and performance optimizer
- Create data browser for all PSA entities
- Add navigation components and UI improvements
- Implement background processing and sync services
- Add comprehensive documentation and migration scripts
- Update configuration items with multi-system support
- Enhance contact management and purchase history
- Add issue type assignment and LLM analyzer
- Improve error handling and logging utilities
2025-11-19 14:18:16 -05:00

219 lines
5.3 KiB
TypeScript

/**
* API Helper Utilities
* Common utilities for API endpoints including query parameter parsing
*/
import { NextRequest } from 'next/server';
export interface QueryOptions {
page?: number;
limit?: number;
offset?: number;
includeDeleted?: boolean;
sort?: string;
order?: 'ASC' | 'DESC';
filters?: Record<string, any>;
}
export interface PaginationInfo {
page: number;
limit: number;
offset: number;
total: number;
totalPages: number;
hasMore: boolean;
hasPrevious: boolean;
}
/**
* Parse query parameters from Next.js request
* @param request Next.js request object
* @param defaults Default values for query options
* @returns Parsed query options
*/
export function parseQueryParams(
request: NextRequest,
defaults: Partial<QueryOptions> = {}
): QueryOptions {
const searchParams = request.nextUrl.searchParams;
// Parse pagination
const page = parseInt(searchParams.get('page') || String(defaults.page || 1));
const limit = parseInt(searchParams.get('limit') || String(defaults.limit || 100));
const offset = (page - 1) * limit;
// Parse includeDeleted flag
const includeDeletedParam = searchParams.get('includeDeleted');
const includeDeleted = includeDeletedParam !== null
? includeDeletedParam === 'true'
: defaults.includeDeleted || false;
// Parse sorting
const sort = searchParams.get('sort') || defaults.sort || 'id';
const orderParam = searchParams.get('order')?.toUpperCase();
const order = (orderParam === 'ASC' || orderParam === 'DESC') ? orderParam : (defaults.order || 'ASC');
// Parse filters
const filters: Record<string, any> = { ...defaults.filters };
// Get all search params and treat unknown params as filters
searchParams.forEach((value, key) => {
// Skip known pagination/sorting params
if (['page', 'limit', 'includeDeleted', 'sort', 'order'].includes(key)) {
return;
}
// Parse filter value
filters[key] = parseFilterValue(value);
});
return {
page,
limit,
offset,
includeDeleted,
sort,
order,
filters,
};
}
/**
* Parse filter value to appropriate type
* @param value String value from query parameter
* @returns Parsed value (boolean, number, or string)
*/
function parseFilterValue(value: string): any {
// Boolean
if (value === 'true') return true;
if (value === 'false') return false;
// Number
if (/^\d+$/.test(value)) return parseInt(value);
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);
// Null
if (value === 'null') return null;
// String (default)
return value;
}
/**
* Build WHERE clause from filters
* @param filters Filter object
* @param includeDeleted Whether to include deleted records
* @returns WHERE clause object
*/
export function buildWhereClause(
filters: Record<string, any>,
includeDeleted: boolean = false
): Record<string, any> {
const where: Record<string, any> = { ...filters };
// Always exclude soft-deleted records unless explicitly requested
if (!includeDeleted) {
where.is_deleted = false;
}
return where;
}
/**
* Build ORDER BY clause
* @param sort Sort field
* @param order Sort order (ASC/DESC)
* @returns ORDER BY string
*/
export function buildOrderByClause(sort: string, order: 'ASC' | 'DESC'): string {
// Sanitize sort field to prevent SQL injection
const sanitizedSort = sort.replace(/[^a-zA-Z0-9_]/g, '');
return `${sanitizedSort} ${order}`;
}
/**
* Create pagination info object
* @param page Current page number
* @param limit Records per page
* @param total Total record count
* @returns Pagination information
*/
export function createPaginationInfo(
page: number,
limit: number,
total: number
): PaginationInfo {
const offset = (page - 1) * limit;
const totalPages = Math.ceil(total / limit);
return {
page,
limit,
offset,
total,
totalPages,
hasMore: page < totalPages,
hasPrevious: page > 1,
};
}
/**
* Validate query parameters
* @param options Query options to validate
* @throws Error if validation fails
*/
export function validateQueryParams(options: QueryOptions): void {
if (options.page && options.page < 1) {
throw new Error('Page must be greater than 0');
}
if (options.limit && (options.limit < 1 || options.limit > 1000)) {
throw new Error('Limit must be between 1 and 1000');
}
if (options.sort && !/^[a-zA-Z0-9_]+$/.test(options.sort)) {
throw new Error('Invalid sort field');
}
}
/**
* Format API response with data and pagination
* @param data Data array
* @param pagination Pagination info
* @param meta Additional metadata
* @returns Formatted response object
*/
export function formatApiResponse<T>(
data: T[],
pagination: PaginationInfo,
meta?: Record<string, any>
) {
return {
data,
pagination,
meta: {
timestamp: new Date().toISOString(),
...meta,
},
};
}
/**
* Handle API errors consistently
* @param error Error object
* @param context Error context
* @returns Error response object
*/
export function handleApiError(error: any, context?: string) {
console.error(`API Error${context ? ` (${context})` : ''}:`, error);
const message = error instanceof Error ? error.message : 'An unexpected error occurred';
const statusCode = error.statusCode || 500;
return {
error: message,
context,
timestamp: new Date().toISOString(),
statusCode,
};
}