Просмотр исходного кода

✨ feat(routes): 添加路由只读模式支持

- 添加readOnly选项,控制是否只注册GET路由
- 重构路由注册逻辑,根据readOnly值决定路由注册范围

♻️ refactor(routes): 优化数据解析与错误处理

- 引入parseWithAwait工具函数处理异步数据解析
- 改进Zod错误处理,使用JSON.parse(error.message)获取详细错误信息
- 统一列表和详情接口的数据解析方式

📦 build(routes): 添加parseWithAwait工具函数

- 实现支持异步数据解析的工具函数
- 处理嵌套Promise的解析问题
- 提供按路径解析Promise的功能
yourname 7 месяцев назад
Родитель
Сommit
eedeba449c

+ 202 - 121
src/server/utils/generic-crud.routes.ts

@@ -5,6 +5,7 @@ import { ErrorSchema } from './errorHandler';
 import { AuthContext } from '../types/context';
 import { ObjectLiteral } from 'typeorm';
 import { AppDataSource } from '../data-source';
+import { parseWithAwait } from './parseWithAwait';
 
 export function createCrudRoutes<
   T extends ObjectLiteral,
@@ -13,7 +14,7 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
   
   // 创建CRUD服务实例
   // 抽象类不能直接实例化,需要创建具体实现类
@@ -217,133 +218,213 @@ export function createCrudRoutes<
   });
   
   // 注册路由处理函数
-  const routes = app
-    .openapi(listRoute, async (c) => {
-      try {
-        const query = c.req.valid('query') as any;
-        const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
-        
-        // 构建排序对象
-        const order: any = {};
-        if (sortBy) {
-          order[sortBy] = sortOrder || 'DESC';
-        } else {
-          order['id'] = 'DESC';
-        }
-        
-        // 解析筛选条件
-        let parsedFilters: any = undefined;
-        if (filters) {
-          try {
-            parsedFilters = JSON.parse(filters);
-          } catch (e) {
-            return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+  
+  // 只读模式下只注册 GET 路由
+  if (!readOnly) {
+    // 完整 CRUD 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
           }
+          
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+          
+          return c.json({
+            // data: z.array(listSchema).parse(data),
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
         }
-        
-        const [data, total] = await crudService.getList(
-          page,
-          pageSize,
-          keyword,
-          searchFields,
-          undefined,
-          relations || [],
-          order,
-          parsedFilters
-        );
-        
-        return c.json({
-          data: z.array(listSchema).parse(data),
-          pagination: { total, current: page, pageSize }
-        }, 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '获取列表失败'
-        }, 500);
-      }
-    })
-    .openapi(createRouteDef, async (c: any) => {
-      try {
-        const data = c.req.valid('json');
-        const user = c.get('user');
-        const result = await crudService.create(data, user?.id);
-        return c.json(result, 201);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '创建资源失败'
-        }, 500);
-      }
-    })
-    .openapi(getRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const result = await crudService.getById(id, relations || []);
-        
-        if (!result) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      })
+      .openapi(createRouteDef, async (c: any) => {
+        try {
+          const data = c.req.valid('json');
+          const user = c.get('user');
+          const result = await crudService.create(data, user?.id);
+          return c.json(result, 201);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '创建资源失败'
+          }, 500);
         }
-        
-        return c.json(getSchema.parse(result), 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
+      })
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          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) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '获取资源失败'
-        }, 500);
-      }
-    })
-    .openapi(updateRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const data = c.req.valid('json');
-        const user = c.get('user');
-        const result = await crudService.update(id, data, user?.id);
-        
-        if (!result) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      })
+      .openapi(updateRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const data = c.req.valid('json');
+          const user = c.get('user');
+          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) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '更新资源失败'
+          }, 500);
         }
-        
-        return c.json(result, 200);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
+      })
+      .openapi(deleteRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          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) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '删除资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '更新资源失败'
-        }, 500);
-      }
-    })
-    .openapi(deleteRouteDef, async (c: any) => {
-      try {
-        const { id } = c.req.valid('param');
-        const success = await crudService.delete(id);
-        
-        if (!success) {
-          return c.json({ code: 404, message: '资源不存在' }, 404);
+      });
+
+    return routes;
+  } else {
+    // 只读模式,只注册 GET 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
+          }
+          
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+          
+          return c.json({
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
         }
-        
-        return c.body(null, 204);
-      } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
+      })
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          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) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
         }
-        return c.json({
-          code: 500,
-          message: error instanceof Error ? error.message : '删除资源失败'
-        }, 500);
-      }
-    });
+      });
+    return routes;
+  }
   
-  return routes;
 }

+ 1 - 0
src/server/utils/generic-crud.service.ts

@@ -289,4 +289,5 @@ export type CrudOptions<
   middleware?: any[];
   userTracking?: UserTrackingOptions;
   relationFields?: RelationFieldOptions;
+  readOnly?: boolean;
 };

+ 59 - 0
src/server/utils/parseWithAwait.ts

@@ -0,0 +1,59 @@
+import { z } from '@hono/zod-openapi';
+
+export async function parseWithAwait<T>(schema: z.ZodSchema<T>, data: unknown): Promise<T> {  
+    // 先尝试同步解析,捕获 Promise 错误  
+    const syncResult = schema.safeParse(data);  
+      
+    if (!syncResult.success) {  
+      // 提取 Promise 错误的路径信息  
+      const promiseErrors = syncResult.error.issues.filter(issue =>   
+        issue.code === 'invalid_type' &&   
+        issue.message.includes('received Promise')  
+      );  
+        
+      if (promiseErrors.length > 0) {  
+        // 根据路径直接 await Promise  
+        const processedData = await resolvePromisesByPath(data, promiseErrors);  
+          
+        // 重新解析处理后的数据  
+        return schema.parse(processedData) as T;  
+      }  
+        
+      throw syncResult.error;  
+    }  
+      
+    return syncResult.data as T;  
+  }  
+    
+  async function resolvePromisesByPath(data: any, promiseErrors: any[]): Promise<any> {  
+    const clonedData = JSON.parse(JSON.stringify(data, (key, value) => {  
+      // 保留 Promise 对象,不进行序列化  
+      return typeof value?.then === 'function' ? value : value;  
+    }));  
+      
+    // 根据错误路径逐个处理 Promise  
+    for (const error of promiseErrors) {  
+      const path = error.path;  
+      const promiseValue = getValueByPath(data, path);  
+        
+      if (promiseValue && typeof promiseValue.then === 'function') {  
+        const resolvedValue = await promiseValue;  
+        setValueByPath(clonedData, path, resolvedValue);  
+      }  
+    }  
+      
+    return clonedData;  
+  }  
+    
+  function getValueByPath(obj: any, path: (string | number)[]): any {  
+    return path.reduce((current, key) => current?.[key], obj);  
+  }  
+    
+  function setValueByPath(obj: any, path: (string | number)[], value: any): void {  
+    const lastKey = path[path.length - 1];  
+    const parentPath = path.slice(0, -1);  
+    const parent = getValueByPath(obj, parentPath);  
+    if (parent) {  
+      parent[lastKey] = value;  
+    }  
+  }