- 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
219 lines
5.3 KiB
TypeScript
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,
|
|
};
|
|
}
|