generic-crud.routes.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
  2. import { z } from '@hono/zod-openapi';
  3. import { GenericCrudService, CrudOptions } from './generic-crud.service';
  4. import { ErrorSchema } from './errorHandler';
  5. import { AuthContext } from '../types/context';
  6. import { ObjectLiteral } from 'typeorm';
  7. import { AppDataSource } from '../data-source';
  8. export function createCrudRoutes<T extends ObjectLiteral>(options: CrudOptions<T>) {
  9. const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, middleware = [] } = options;
  10. // 创建CRUD服务实例
  11. // 抽象类不能直接实例化,需要创建具体实现类
  12. class ConcreteCrudService extends GenericCrudService<T> {
  13. constructor() {
  14. super(AppDataSource, entity);
  15. }
  16. }
  17. const crudService = new ConcreteCrudService();
  18. // 创建路由实例
  19. const app = new OpenAPIHono<AuthContext>();
  20. // 分页查询路由
  21. const listRoute = createRoute({
  22. method: 'get',
  23. path: '/',
  24. middleware,
  25. request: {
  26. query: z.object({
  27. page: z.coerce.number().int().positive().default(1).openapi({
  28. example: 1,
  29. description: '页码,从1开始'
  30. }),
  31. pageSize: z.coerce.number().int().positive().default(10).openapi({
  32. example: 10,
  33. description: '每页数量'
  34. }),
  35. keyword: z.string().optional().openapi({
  36. example: '搜索关键词',
  37. description: '搜索关键词'
  38. }),
  39. sortBy: z.string().optional().openapi({
  40. example: 'createdAt',
  41. description: '排序字段'
  42. }),
  43. sortOrder: z.enum(['ASC', 'DESC']).optional().default('DESC').openapi({
  44. example: 'DESC',
  45. description: '排序方向'
  46. })
  47. })
  48. },
  49. responses: {
  50. 200: {
  51. description: '成功获取列表',
  52. content: {
  53. 'application/json': {
  54. schema: z.object({
  55. data: z.array(listSchema),
  56. pagination: z.object({
  57. total: z.number().openapi({ example: 100, description: '总记录数' }),
  58. current: z.number().openapi({ example: 1, description: '当前页码' }),
  59. pageSize: z.number().openapi({ example: 10, description: '每页数量' })
  60. })
  61. })
  62. }
  63. }
  64. },
  65. 400: {
  66. description: '参数错误',
  67. content: { 'application/json': { schema: ErrorSchema } }
  68. },
  69. 500: {
  70. description: '服务器错误',
  71. content: { 'application/json': { schema: ErrorSchema } }
  72. }
  73. }
  74. });
  75. // 创建资源路由
  76. const createRouteDef = createRoute({
  77. method: 'post',
  78. path: '/',
  79. middleware,
  80. request: {
  81. body: {
  82. content: {
  83. 'application/json': { schema: createSchema }
  84. }
  85. }
  86. },
  87. responses: {
  88. 201: {
  89. description: '创建成功',
  90. content: { 'application/json': { schema: getSchema } }
  91. },
  92. 400: {
  93. description: '输入数据无效',
  94. content: { 'application/json': { schema: ErrorSchema } }
  95. },
  96. 500: {
  97. description: '服务器错误',
  98. content: { 'application/json': { schema: ErrorSchema } }
  99. }
  100. }
  101. });
  102. // 获取单个资源路由
  103. const getRouteDef = createRoute({
  104. method: 'get',
  105. path: '/{id}',
  106. middleware,
  107. request: {
  108. params: z.object({
  109. id: z.coerce.number().openapi({
  110. param: { name: 'id', in: 'path' },
  111. example: 1,
  112. description: '资源ID'
  113. })
  114. })
  115. },
  116. responses: {
  117. 200: {
  118. description: '成功获取详情',
  119. content: { 'application/json': { schema: getSchema } }
  120. },
  121. 400: {
  122. description: '资源不存在',
  123. content: { 'application/json': { schema: ErrorSchema } }
  124. },
  125. 404: {
  126. description: '参数验证失败',
  127. content: { 'application/json': { schema: ErrorSchema } }
  128. },
  129. 500: {
  130. description: '服务器错误',
  131. content: { 'application/json': { schema: ErrorSchema } }
  132. }
  133. }
  134. });
  135. // 更新资源路由
  136. const updateRouteDef = createRoute({
  137. method: 'put',
  138. path: '/{id}',
  139. middleware,
  140. request: {
  141. params: z.object({
  142. id: z.coerce.number().openapi({
  143. param: { name: 'id', in: 'path' },
  144. example: 1,
  145. description: '资源ID'
  146. })
  147. }),
  148. body: {
  149. content: {
  150. 'application/json': { schema: updateSchema }
  151. }
  152. }
  153. },
  154. responses: {
  155. 200: {
  156. description: '更新成功',
  157. content: { 'application/json': { schema: getSchema } }
  158. },
  159. 400: {
  160. description: '无效输入',
  161. content: { 'application/json': { schema: ErrorSchema } }
  162. },
  163. 404: {
  164. description: '资源不存在',
  165. content: { 'application/json': { schema: ErrorSchema } }
  166. },
  167. 500: {
  168. description: '服务器错误',
  169. content: { 'application/json': { schema: ErrorSchema } }
  170. }
  171. }
  172. });
  173. // 删除资源路由
  174. const deleteRouteDef = createRoute({
  175. method: 'delete',
  176. path: '/{id}',
  177. middleware,
  178. request: {
  179. params: z.object({
  180. id: z.coerce.number().openapi({
  181. param: { name: 'id', in: 'path' },
  182. example: 1,
  183. description: '资源ID'
  184. })
  185. })
  186. },
  187. responses: {
  188. 204: { description: '删除成功' },
  189. 404: {
  190. description: '资源不存在',
  191. content: { 'application/json': { schema: ErrorSchema } }
  192. },
  193. 500: {
  194. description: '服务器错误',
  195. content: { 'application/json': { schema: ErrorSchema } }
  196. }
  197. }
  198. });
  199. // 注册路由处理函数
  200. const routes = app
  201. .openapi(listRoute, async (c) => {
  202. try {
  203. const { page, pageSize, keyword, sortBy, sortOrder } = c.req.valid('query');
  204. // 构建排序对象
  205. // 使用Record和类型断言解决泛型索引写入问题
  206. const order: Partial<Record<keyof T, 'ASC' | 'DESC'>> = {};
  207. if (sortBy) {
  208. (order as Record<string, 'ASC' | 'DESC'>)[sortBy] = sortOrder || 'DESC';
  209. } else {
  210. // 默认按id降序排序
  211. (order as Record<string, 'ASC' | 'DESC'>)['id'] = 'DESC';
  212. }
  213. const [data, total] = await crudService.getList(
  214. page,
  215. pageSize,
  216. keyword,
  217. searchFields,
  218. undefined, // where条件
  219. [], // relations
  220. order
  221. );
  222. return c.json({
  223. data: data as any[],
  224. pagination: { total, current: page, pageSize }
  225. }, 200);
  226. } catch (error) {
  227. if (error instanceof z.ZodError) {
  228. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  229. }
  230. return c.json({
  231. code: 500,
  232. message: error instanceof Error ? error.message : '获取列表失败'
  233. }, 500);
  234. }
  235. })
  236. .openapi(createRouteDef, async (c) => {
  237. try {
  238. const data = c.req.valid('json');
  239. const result = await crudService.create(data);
  240. return c.json(result, 201);
  241. } catch (error) {
  242. if (error instanceof z.ZodError) {
  243. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  244. }
  245. return c.json({
  246. code: 500,
  247. message: error instanceof Error ? error.message : '创建资源失败'
  248. }, 500);
  249. }
  250. })
  251. .openapi(getRouteDef, async (c) => {
  252. try {
  253. const { id } = c.req.valid('param');
  254. const result = await crudService.getById(id);
  255. if (!result) {
  256. return c.json({ code: 404, message: '资源不存在' }, 404);
  257. }
  258. return c.json(result, 200);
  259. } catch (error) {
  260. if (error instanceof z.ZodError) {
  261. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  262. }
  263. return c.json({
  264. code: 500,
  265. message: error instanceof Error ? error.message : '获取资源失败'
  266. }, 500);
  267. }
  268. })
  269. .openapi(updateRouteDef, async (c) => {
  270. try {
  271. const { id } = c.req.valid('param');
  272. const data = c.req.valid('json');
  273. const result = await crudService.update(id, data);
  274. if (!result) {
  275. return c.json({ code: 404, message: '资源不存在' }, 404);
  276. }
  277. return c.json(result, 200);
  278. } catch (error) {
  279. if (error instanceof z.ZodError) {
  280. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  281. }
  282. if (error instanceof z.ZodError) {
  283. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  284. }
  285. return c.json({
  286. code: 500,
  287. message: error instanceof Error ? error.message : '更新资源失败'
  288. }, 500);
  289. }
  290. })
  291. .openapi(deleteRouteDef, async (c) => {
  292. try {
  293. const { id } = c.req.valid('param');
  294. const success = await crudService.delete(id);
  295. if (!success) {
  296. return c.json({ code: 404, message: '资源不存在' }, 404);
  297. }
  298. return c.body(null, 204) as unknown as Response;
  299. } catch (error) {
  300. if (error instanceof z.ZodError) {
  301. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  302. }
  303. if (error instanceof z.ZodError) {
  304. return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
  305. }
  306. return c.json({
  307. code: 500,
  308. message: error instanceof Error ? error.message : '删除资源失败'
  309. }, 500);
  310. }
  311. });
  312. return routes;
  313. }