UNPKG

arvox-backend

Version:

Un framework backend moderne et modulaire basé sur Hono, TypeScript et l'architecture hexagonale avec authentification Better Auth + Drizzle intégrée

377 lines 13.9 kB
import { OpenAPIHono } from '@hono/zod-openapi'; import { createRoute } from '@hono/zod-openapi'; import { z } from 'zod'; import { ResponseUtil } from '../utils/response.util'; import { PaginationUtil } from '../utils/pagination.util'; /** * Base controller class providing common HTTP functionality * All controllers should extend this class for consistent behavior */ export class BaseController { controller; responseUtil; paginationUtil; constructor() { this.controller = new OpenAPIHono(); this.responseUtil = new ResponseUtil(); this.paginationUtil = new PaginationUtil(); this.initRoutes(); } /** * Handle standard success response * @param data - Data to return * @param status - HTTP status code (default: 200) * @returns Formatted success response */ success(data, status = 200) { return this.responseUtil.success(data, status); } /** * Handle standard error response * @param error - Error message or Error object * @param status - HTTP status code (default: 400) * @returns Formatted error response */ error(error, status = 400) { return this.responseUtil.error(error, status); } /** * Handle paginated response * @param items - Array of items * @param total - Total count of items * @param page - Current page number * @param limit - Items per page * @param status - HTTP status code (default: 200) * @returns Formatted paginated response */ paginated(items, total, page, limit, status = 200) { return this.responseUtil.paginated(items, total, page, limit, status); } /** * Extract pagination parameters from request context * @param c - Hono context * @returns Pagination parameters with defaults */ getPaginationParams(c) { return this.paginationUtil.extractFromContext(c); } /** * Handle file upload validation * @param file - Uploaded file * @param allowedTypes - Array of allowed MIME types * @param maxSize - Maximum file size in bytes * @throws Error if validation fails */ validateFile(file, allowedTypes, maxSize) { if (!allowedTypes.includes(file.type)) { throw new Error(`File type not allowed. Accepted types: ${allowedTypes.join(', ')}`); } if (file.size > maxSize) { throw new Error(`File size exceeds limit of ${maxSize / 1024 / 1024}MB`); } } /** * Handle multipart form data extraction * @param c - Hono context * @returns Promise with form data object */ async extractFormData(c) { const body = await c.req.parseBody(); const formData = {}; for (const [key, value] of Object.entries(body)) { if (value instanceof File) { formData[key] = value; } else if (typeof value === 'string') { // Try to parse JSON strings try { formData[key] = JSON.parse(value); } catch { formData[key] = value; } } else { formData[key] = value; } } return formData; } /** * Extract user information from authenticated context * @param c - Hono context * @returns User information or null if not authenticated */ getAuthenticatedUser(c) { return c.get('user') || null; } /** * Check if user has required role * @param c - Hono context * @param requiredRoles - Array of required roles * @returns Boolean indicating if user has required role */ hasRole(c, requiredRoles) { const user = this.getAuthenticatedUser(c); if (!user || !user.role) return false; return requiredRoles.includes(user.role.name); } /** * Create a simplified POST route with automatic OpenAPI configuration * @param path - Route path * @param schema - Request and response schemas * @param handler - Route handler function * @param options - Additional options */ createPostRoute(path, schema, handler, options) { const route = createRoute({ method: 'post', path, tags: [schema.tag || this.getDefaultTag()], summary: schema.summary || `Create ${this.getResourceName()}`, description: schema.description, request: { body: { content: { [options?.multipart ? 'multipart/form-data' : 'application/json']: { schema: schema.request } } } }, responses: { [options?.statusCode || 201]: { description: `${this.getResourceName()} created successfully`, content: { 'application/json': { schema: z.object({ success: z.boolean(), data: schema.response }) } } }, 400: this.getErrorResponse('Validation error'), ...(options?.security ? { 401: this.getErrorResponse('Unauthorized') } : {}) } }); this.controller.openapi(route, async (c) => { const body = c.req.valid('json'); return await handler(c, body); }); } /** * Create a simplified GET route for listing resources with pagination * @param path - Route path * @param schema - Response schema * @param handler - Route handler function * @param options - Additional options */ createListRoute(path, schema, handler, options) { const route = createRoute({ method: 'get', path, tags: [schema.tag || this.getDefaultTag()], summary: schema.summary || `Get ${this.getResourceName()} list`, description: schema.description, request: { query: z.object({ page: z.string().optional().transform((val) => val ? parseInt(val) : 1), limit: z.string().optional().transform((val) => val ? parseInt(val) : 10), search: z.string().optional(), sort: z.string().optional() }) }, responses: { 200: { description: `${this.getResourceName()} list retrieved successfully`, content: { 'application/json': { schema: z.object({ success: z.boolean(), data: z.object({ items: z.array(schema.response), pagination: z.object({ total: z.number(), page: z.number(), limit: z.number(), totalPages: z.number(), hasNext: z.boolean(), hasPrev: z.boolean() }) }) }) } } }, 400: this.getErrorResponse('Bad request'), ...(options?.security ? { 401: this.getErrorResponse('Unauthorized') } : {}) } }); this.controller.openapi(route, async (c) => { const query = c.req.valid('query'); return await handler(c, query); }); } /** * Create a simplified GET route for single resource * @param path - Route path (should include {id} parameter) * @param schema - Response schema * @param handler - Route handler function * @param options - Additional options */ createGetByIdRoute(path, schema, handler, options) { const route = createRoute({ method: 'get', path, tags: [schema.tag || this.getDefaultTag()], summary: schema.summary || `Get ${this.getResourceName()} by ID`, description: schema.description, request: { params: z.object({ id: z.string().uuid('ID must be a valid UUID') }) }, responses: { 200: { description: `${this.getResourceName()} retrieved successfully`, content: { 'application/json': { schema: z.object({ success: z.boolean(), data: schema.response }) } } }, 404: this.getErrorResponse('Resource not found'), ...(options?.security ? { 401: this.getErrorResponse('Unauthorized') } : {}) } }); this.controller.openapi(route, async (c) => { const { id } = c.req.valid('param'); return await handler(c, id); }); } /** * Create a simplified PUT route * @param path - Route path (should include {id} parameter) * @param schema - Request and response schemas * @param handler - Route handler function * @param options - Additional options */ createPutRoute(path, schema, handler, options) { const route = createRoute({ method: 'put', path, tags: [schema.tag || this.getDefaultTag()], summary: schema.summary || `Update ${this.getResourceName()}`, description: schema.description, request: { params: z.object({ id: z.string().uuid('ID must be a valid UUID') }), body: { content: { [options?.multipart ? 'multipart/form-data' : 'application/json']: { schema: schema.request } } } }, responses: { 200: { description: `${this.getResourceName()} updated successfully`, content: { 'application/json': { schema: z.object({ success: z.boolean(), data: schema.response }) } } }, 400: this.getErrorResponse('Validation error'), 404: this.getErrorResponse('Resource not found'), ...(options?.security ? { 401: this.getErrorResponse('Unauthorized') } : {}) } }); this.controller.openapi(route, async (c) => { const { id } = c.req.valid('param'); const body = c.req.valid('json'); return await handler(c, id, body); }); } /** * Create a simplified DELETE route * @param path - Route path (should include {id} parameter) * @param schema - Optional configuration * @param handler - Route handler function * @param options - Additional options */ createDeleteRoute(path, schema, handler, options) { const route = createRoute({ method: 'delete', path, tags: [schema.tag || this.getDefaultTag()], summary: schema.summary || `Delete ${this.getResourceName()}`, description: schema.description, request: { params: z.object({ id: z.string().uuid('ID must be a valid UUID') }) }, responses: { 200: { description: `${this.getResourceName()} deleted successfully`, content: { 'application/json': { schema: z.object({ success: z.boolean(), data: z.object({ deleted: z.boolean() }) }) } } }, 404: this.getErrorResponse('Resource not found'), ...(options?.security ? { 401: this.getErrorResponse('Unauthorized') } : {}) } }); this.controller.openapi(route, async (c) => { const { id } = c.req.valid('param'); return await handler(c, id); }); } /** * Get default tag for routes (can be overridden in child classes) */ getDefaultTag() { return this.constructor.name.replace('Controller', ''); } /** * Get resource name for documentation (can be overridden in child classes) */ getResourceName() { return this.getDefaultTag().toLowerCase(); } /** * Get standardized error response schema */ getErrorResponse(description) { return { description, content: { 'application/json': { schema: z.object({ success: z.boolean(), error: z.string() }) } } }; } } //# sourceMappingURL=base-controller.js.map