generic-crud.routes.ts 9.9 KB

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