365 lines
8.0 KiB
TypeScript
365 lines
8.0 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import {
|
|
ApiSuccessResponse,
|
|
ApiErrorResponse,
|
|
PaginatedResponse,
|
|
ResponseMeta,
|
|
PaginationMeta,
|
|
ApiErrorCode,
|
|
} from "./types";
|
|
|
|
/**
|
|
* API Response Helpers for FitAI Admin API
|
|
*
|
|
* Provides consistent response formatting across all API endpoints.
|
|
*/
|
|
|
|
/**
|
|
* Generate a unique request ID
|
|
*/
|
|
function generateRequestId(): string {
|
|
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
}
|
|
|
|
/**
|
|
* Create response metadata
|
|
*/
|
|
function createMeta(options?: {
|
|
requestId?: string;
|
|
pagination?: PaginationMeta;
|
|
}): ResponseMeta {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
requestId: options?.requestId || generateRequestId(),
|
|
...(options?.pagination && { pagination: options.pagination }),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a standardized success response
|
|
*
|
|
* @param data - The response data
|
|
* @param options - Optional metadata configuration
|
|
* @returns NextResponse with standardized format
|
|
*
|
|
* @example
|
|
* return successResponse({ user: { id: '123', email: 'user@example.com' } });
|
|
*
|
|
* @example
|
|
* return successResponse({ message: 'User created' }, { status: 201 });
|
|
*/
|
|
export function successResponse<T>(
|
|
data: T,
|
|
options?: {
|
|
status?: number;
|
|
requestId?: string;
|
|
headers?: HeadersInit;
|
|
},
|
|
): NextResponse<ApiSuccessResponse<T>> {
|
|
const response: ApiSuccessResponse<T> = {
|
|
success: true,
|
|
data,
|
|
meta: createMeta({ requestId: options?.requestId }),
|
|
};
|
|
|
|
return NextResponse.json(response, {
|
|
status: options?.status || 200,
|
|
headers: options?.headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a standardized paginated response
|
|
*
|
|
* @param data - Array of items to return
|
|
* @param pagination - Pagination metadata
|
|
* @param options - Optional configuration
|
|
* @returns NextResponse with paginated format
|
|
*
|
|
* @example
|
|
* const users = await db.getUsers();
|
|
* return paginatedResponse(users, {
|
|
* page: 1,
|
|
* pageSize: 20,
|
|
* totalItems: 100,
|
|
* totalPages: 5,
|
|
* hasNextPage: true,
|
|
* hasPreviousPage: false,
|
|
* });
|
|
*/
|
|
export function paginatedResponse<T>(
|
|
data: T[],
|
|
pagination: PaginationMeta,
|
|
options?: {
|
|
requestId?: string;
|
|
headers?: HeadersInit;
|
|
},
|
|
): NextResponse<PaginatedResponse<T>> {
|
|
const response: PaginatedResponse<T> = {
|
|
success: true,
|
|
data,
|
|
meta: {
|
|
...createMeta({ requestId: options?.requestId }),
|
|
pagination,
|
|
},
|
|
};
|
|
|
|
return NextResponse.json(response, {
|
|
status: 200,
|
|
headers: options?.headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a standardized error response
|
|
*
|
|
* @param message - Error message
|
|
* @param options - Error configuration
|
|
* @returns NextResponse with error format
|
|
*
|
|
* @example
|
|
* return errorResponse('User not found', {
|
|
* status: 404,
|
|
* code: ApiErrorCode.NOT_FOUND,
|
|
* });
|
|
*
|
|
* @example
|
|
* return errorResponse('Validation failed', {
|
|
* status: 400,
|
|
* code: ApiErrorCode.VALIDATION_ERROR,
|
|
* details: { email: ['Invalid email format'] },
|
|
* });
|
|
*/
|
|
export function errorResponse(
|
|
message: string,
|
|
options?: {
|
|
status?: number;
|
|
code?: ApiErrorCode | string;
|
|
details?: Record<string, string[]>;
|
|
requestId?: string;
|
|
headers?: HeadersInit;
|
|
},
|
|
): NextResponse<ApiErrorResponse> {
|
|
const response: ApiErrorResponse = {
|
|
success: false,
|
|
error: {
|
|
message,
|
|
code: options?.code,
|
|
details: options?.details,
|
|
},
|
|
meta: createMeta({ requestId: options?.requestId }),
|
|
};
|
|
|
|
return NextResponse.json(response, {
|
|
status: options?.status || 500,
|
|
headers: options?.headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 400 Bad Request response
|
|
*/
|
|
export function badRequestResponse(
|
|
message: string = "Bad request",
|
|
details?: Record<string, string[]>,
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 400,
|
|
code: ApiErrorCode.INVALID_INPUT,
|
|
details,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 401 Unauthorized response
|
|
*/
|
|
export function unauthorizedResponse(
|
|
message: string = "Unauthorized",
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 401,
|
|
code: ApiErrorCode.UNAUTHORIZED,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 403 Forbidden response
|
|
*/
|
|
export function forbiddenResponse(
|
|
message: string = "Forbidden",
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 403,
|
|
code: ApiErrorCode.FORBIDDEN,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 404 Not Found response
|
|
*/
|
|
export function notFoundResponse(
|
|
message: string = "Resource not found",
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 404,
|
|
code: ApiErrorCode.NOT_FOUND,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 409 Conflict response
|
|
*/
|
|
export function conflictResponse(
|
|
message: string = "Resource conflict",
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 409,
|
|
code: ApiErrorCode.CONFLICT,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a 500 Internal Server Error response
|
|
*/
|
|
export function internalErrorResponse(
|
|
message: string = "Internal server error",
|
|
headers?: HeadersInit,
|
|
): NextResponse<ApiErrorResponse> {
|
|
return errorResponse(message, {
|
|
status: 500,
|
|
code: ApiErrorCode.INTERNAL_ERROR,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calculate pagination metadata from array and page parameters
|
|
*
|
|
* @param totalItems - Total number of items
|
|
* @param page - Current page number (1-indexed)
|
|
* @param pageSize - Number of items per page
|
|
* @returns Pagination metadata
|
|
*
|
|
* @example
|
|
* const pagination = calculatePagination(100, 1, 20);
|
|
* // { page: 1, pageSize: 20, totalItems: 100, totalPages: 5, hasNextPage: true, hasPreviousPage: false }
|
|
*/
|
|
export function calculatePagination(
|
|
totalItems: number,
|
|
page: number,
|
|
pageSize: number,
|
|
): PaginationMeta {
|
|
const totalPages = Math.ceil(totalItems / pageSize);
|
|
|
|
return {
|
|
page,
|
|
pageSize,
|
|
totalItems,
|
|
totalPages,
|
|
hasNextPage: page < totalPages,
|
|
hasPreviousPage: page > 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Paginate an array of items
|
|
*
|
|
* @param items - Array to paginate
|
|
* @param page - Current page (1-indexed)
|
|
* @param pageSize - Items per page
|
|
* @returns Paginated slice of items and metadata
|
|
*
|
|
* @example
|
|
* const users = await db.getAllUsers();
|
|
* const { items, pagination } = paginateArray(users, 1, 20);
|
|
* return paginatedResponse(items, pagination);
|
|
*/
|
|
export function paginateArray<T>(
|
|
items: T[],
|
|
page: number,
|
|
pageSize: number,
|
|
): {
|
|
items: T[];
|
|
pagination: PaginationMeta;
|
|
} {
|
|
const startIndex = (page - 1) * pageSize;
|
|
const endIndex = startIndex + pageSize;
|
|
const paginatedItems = items.slice(startIndex, endIndex);
|
|
|
|
return {
|
|
items: paginatedItems,
|
|
pagination: calculatePagination(items.length, page, pageSize),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add CORS headers to response
|
|
*
|
|
* @param headers - Existing headers or undefined
|
|
* @returns Headers with CORS added
|
|
*/
|
|
export function addCorsHeaders(headers?: HeadersInit): HeadersInit {
|
|
const corsHeaders = {
|
|
"Access-Control-Allow-Origin": "*",
|
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
};
|
|
|
|
if (!headers) {
|
|
return corsHeaders;
|
|
}
|
|
|
|
if (headers instanceof Headers) {
|
|
const newHeaders = new Headers(headers);
|
|
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
newHeaders.set(key, value);
|
|
});
|
|
return newHeaders;
|
|
}
|
|
|
|
if (Array.isArray(headers)) {
|
|
return [...headers, ...Object.entries(corsHeaders)];
|
|
}
|
|
|
|
return { ...headers, ...corsHeaders };
|
|
}
|
|
|
|
/**
|
|
* Create a 204 No Content response
|
|
*/
|
|
export function noContentResponse(
|
|
headers?: HeadersInit,
|
|
): NextResponse<undefined> {
|
|
return new NextResponse(undefined, {
|
|
status: 204,
|
|
headers,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a standardized created response (201)
|
|
*/
|
|
export function createdResponse<T>(
|
|
data: T,
|
|
options?: {
|
|
requestId?: string;
|
|
headers?: HeadersInit;
|
|
},
|
|
): NextResponse<ApiSuccessResponse<T>> {
|
|
return successResponse(data, {
|
|
status: 201,
|
|
requestId: options?.requestId,
|
|
headers: options?.headers,
|
|
});
|
|
}
|