Jelajahi Sumber

♻️ refactor(crud): 重构通用CRUD路由实现

- 提取ConcreteCrudService为独立导出类,优化代码结构
- 移除createCrudRoutes内部的ConcreteCrudService定义,改为使用外部导入
- 在各CRUD操作中动态创建ConcreteCrudService实例,确保配置正确应用

✅ test(files): 增强文件API集成测试调试能力

- 添加错误响应调试日志,便于问题排查
- 记录响应状态码和错误详情,提高测试可观测性
yourname 2 bulan lalu
induk
melakukan
ddcc36715e

+ 8 - 0
src/server/api/files/__tests__/files.integration.test.ts

@@ -5,10 +5,12 @@ import { FileService } from '@/server/modules/files/file.service';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
 import { fileApiRoutes } from '@/server/api';
 import { AppDataSource } from '@/server/data-source';
+import { ConcreteCrudService } from '@/server/utils/generic-crud.routes';
 
 vi.mock('@/server/modules/files/file.service');
 vi.mock('@/server/middleware/auth.middleware');
 vi.mock('@/server/data-source');
+// vi.mock('@/server/utils/generic-crud.routes');
 
 describe('File API Integration Tests', () => {
   let client: ReturnType<typeof testClient<typeof fileApiRoutes>>['api']['v1'];
@@ -47,6 +49,7 @@ describe('File API Integration Tests', () => {
       await next();
     });
 
+
     client = testClient(fileApiRoutes).api.v1;
   });
 
@@ -512,6 +515,11 @@ describe('File API Integration Tests', () => {
         }
       });
 
+      if (response.status !== 200) {
+        const error = await response.json();
+        console.debug('Error response:', JSON.stringify(error, null, 2));
+        console.debug('Response status:', response.status);
+      }
       expect(response.status).toBe(200);
       const result = await response.json();
       expect(result).toEqual({

+ 57 - 26
src/server/utils/generic-crud.routes.ts

@@ -1,12 +1,19 @@
 import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
-import { GenericCrudService, CrudOptions } from './generic-crud.service';
+import { GenericCrudService, CrudOptions, UserTrackingOptions, RelationFieldOptions } from './generic-crud.service';
 import { ErrorSchema } from './errorHandler';
 import { AuthContext } from '../types/context';
 import { ObjectLiteral } from 'typeorm';
 import { AppDataSource } from '../data-source';
 import { parseWithAwait } from './parseWithAwait';
 
+// 创建具体CRUD服务类
+export class ConcreteCrudService<T extends ObjectLiteral> extends GenericCrudService<T> {
+  constructor(entity: new () => T, options?: { userTracking?: UserTrackingOptions; relationFields?: RelationFieldOptions }) {
+    super(AppDataSource, entity, options);
+  }
+}
+
 export function createCrudRoutes<
   T extends ObjectLiteral,
   CreateSchema extends z.ZodSchema = z.ZodSchema,
@@ -16,15 +23,6 @@ export function createCrudRoutes<
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
   const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
   
-  // 创建CRUD服务实例
-  // 抽象类不能直接实例化,需要创建具体实现类
-  class ConcreteCrudService extends GenericCrudService<T> {
-    constructor() {
-      super(AppDataSource, entity, { userTracking, relationFields });
-    }
-  }
-  const crudService = new ConcreteCrudService();
-  
   // 创建路由实例
   const app = new OpenAPIHono<AuthContext>();
   
@@ -227,7 +225,7 @@ export function createCrudRoutes<
         try {
           const query = c.req.valid('query') as any;
           const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
-          
+
           // 构建排序对象
           const order: any = {};
           if (sortBy) {
@@ -235,7 +233,7 @@ export function createCrudRoutes<
           } else {
             order['id'] = 'DESC';
           }
-          
+
           // 解析筛选条件
           let parsedFilters: any = undefined;
           if (filters) {
@@ -245,7 +243,11 @@ export function createCrudRoutes<
               return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
             }
           }
-          
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+
           const [data, total] = await crudService.getList(
             page,
             pageSize,
@@ -256,7 +258,7 @@ export function createCrudRoutes<
             order,
             parsedFilters
           );
-          
+
           return c.json({
             // data: z.array(listSchema).parse(data),
             data: await parseWithAwait(z.array(listSchema), data),
@@ -276,6 +278,11 @@ export function createCrudRoutes<
         try {
           const data = c.req.valid('json');
           const user = c.get('user');
+
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
           const result = await crudService.create(data, user?.id);
           return c.json(result, 201);
         } catch (error) {
@@ -291,12 +298,17 @@ export function createCrudRoutes<
       .openapi(getRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
           const result = await crudService.getById(id, relations || []);
-          
+
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
-          
+
           // return c.json(await getSchema.parseAsync(result), 200);
           return c.json(await parseWithAwait(getSchema, result), 200);
         } catch (error) {
@@ -314,12 +326,17 @@ export function createCrudRoutes<
           const { id } = c.req.valid('param');
           const data = c.req.valid('json');
           const user = c.get('user');
+
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
           const result = await crudService.update(id, data, user?.id);
-          
+
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
-          
+
           return c.json(result, 200);
         } catch (error) {
           if (error instanceof z.ZodError) {
@@ -334,12 +351,17 @@ export function createCrudRoutes<
       .openapi(deleteRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
           const success = await crudService.delete(id);
-          
+
           if (!success) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
-          
+
           return c.body(null, 204);
         } catch (error) {
           if (error instanceof z.ZodError) {
@@ -360,7 +382,7 @@ export function createCrudRoutes<
         try {
           const query = c.req.valid('query') as any;
           const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
-          
+
           // 构建排序对象
           const order: any = {};
           if (sortBy) {
@@ -368,7 +390,7 @@ export function createCrudRoutes<
           } else {
             order['id'] = 'DESC';
           }
-          
+
           // 解析筛选条件
           let parsedFilters: any = undefined;
           if (filters) {
@@ -378,7 +400,11 @@ export function createCrudRoutes<
               return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
             }
           }
-          
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+
           const [data, total] = await crudService.getList(
             page,
             pageSize,
@@ -389,7 +415,7 @@ export function createCrudRoutes<
             order,
             parsedFilters
           );
-          
+
           return c.json({
             data: await parseWithAwait(z.array(listSchema), data),
             pagination: { total, current: page, pageSize }
@@ -407,12 +433,17 @@ export function createCrudRoutes<
       .openapi(getRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
           const result = await crudService.getById(id, relations || []);
-          
+
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
-          
+
           return c.json(await parseWithAwait(getSchema, result), 200);
         } catch (error) {
           if (error instanceof z.ZodError) {