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
JavaScript
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