generic-crud.routes.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
  2. import { z } from '@hono/zod-openapi';
  3. import { CrudOptions } from './generic-crud.service';
  4. import { ErrorSchema } from './errorHandler';
  5. import { AuthContext } from '../types/context';
  6. import { ObjectLiteral } from 'typeorm';
  7. import { parseWithAwait } from './parseWithAwait';
  8. import { ConcreteCrudService } from './concrete-crud.service';
  9. export function createCrudRoutes<
  10. T extends ObjectLiteral,
  11. CreateSchema extends z.ZodSchema = z.ZodSchema,
  12. UpdateSchema extends z.ZodSchema = z.ZodSchema,
  13. GetSchema extends z.ZodSchema = z.ZodSchema,
  14. ListSchema extends z.ZodSchema = z.ZodSchema
  15. >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
  16. const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
  17. // 创建路由实例
  18. const app = new OpenAPIHono<AuthContext>();
  19. // 分页查询路由
  20. const listRoute = createRoute({
  21. method: 'get',
  22. path: '/',
  23. middleware,
  24. request: {
  25. query: z.object({
  26. page: z.coerce.number<number>().int().positive().default(1).openapi({
  27. example: 1,
  28. description: '页码,从1开始'
  29. }),
  30. pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
  31. example: 10,
  32. description: '每页数量'
  33. }),
  34. keyword: z.string().optional().openapi({
  35. example: '搜索关键词',
  36. description: '搜索关键词'
  37. }),
  38. sortBy: z.string().optional().openapi({
  39. example: 'createdAt',
  40. description: '排序字段'
  41. }),
  42. sortOrder: z.enum(['ASC', 'DESC']).optional().default('DESC').openapi({
  43. example: 'DESC',
  44. description: '排序方向'
  45. }),
  46. // 增强的筛选参数
  47. filters: z.string().optional().openapi({
  48. example: '{"status": 1, "createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}',
  49. description: '筛选条件(JSON字符串),支持精确匹配、范围查询、IN查询等'
  50. })
  51. })
  52. },
  53. responses: {
  54. 200: {
  55. description: '成功获取列表',
  56. content: {
  57. 'application/json': {
  58. schema: z.object({
  59. data: z.array(listSchema),
  60. pagination: z.object({
  61. total: z.number().openapi({ example: 100, description: '总记录数' }),
  62. current: z.number().openapi({ example: 1, description: '当前页码' }),
  63. pageSize: z.number().openapi({ example: 10, description: '每页数量' })
  64. })
  65. })
  66. }
  67. }
  68. },
  69. 400: {
  70. description: '参数错误',
  71. content: { 'application/json': { schema: ErrorSchema } }
  72. },
  73. 500: {
  74. description: '服务器错误',
  75. content: { 'application/json': { schema: ErrorSchema } }
  76. }
  77. }
  78. });
  79. // 创建资源路由
  80. const createRouteDef = createRoute({
  81. method: 'post',
  82. path: '/',
  83. middleware,
  84. request: {
  85. body: {
  86. content: {
  87. 'application/json': { schema: createSchema }
  88. }
  89. }
  90. },
  91. responses: {
  92. 201: {
  93. description: '创建成功',
  94. content: { 'application/json': { schema: getSchema } }
  95. },
  96. 400: {
  97. description: '输入数据无效',
  98. content: { 'application/json': { schema: ErrorSchema } }
  99. },
  100. 500: {
  101. description: '服务器错误',
  102. content: { 'application/json': { schema: ErrorSchema } }
  103. }
  104. }
  105. });
  106. // 获取单个资源路由
  107. const getRouteDef = createRoute({
  108. method: 'get',
  109. path: '/{id}',
  110. middleware,
  111. request: {
  112. params: z.object({
  113. id: z.coerce.number<number>().openapi({
  114. param: { name: 'id', in: 'path' },
  115. example: 1,
  116. description: '资源ID'
  117. })
  118. })
  119. },
  120. responses: {
  121. 200: {
  122. description: '成功获取详情',
  123. content: { 'application/json': { schema: getSchema } }
  124. },
  125. 400: {
  126. description: '资源不存在',
  127. content: { 'application/json': { schema: ErrorSchema } }
  128. },
  129. 404: {
  130. description: '参数验证失败',
  131. content: { 'application/json': { schema: ErrorSchema } }
  132. },
  133. 500: {
  134. description: '服务器错误',
  135. content: { 'application/json': { schema: ErrorSchema } }
  136. }
  137. }
  138. });
  139. // 更新资源路由
  140. const updateRouteDef = createRoute({
  141. method: 'put',
  142. path: '/{id}',
  143. middleware,
  144. request: {
  145. params: z.object({
  146. id: z.coerce.number<number>().openapi({
  147. param: { name: 'id', in: 'path' },
  148. example: 1,
  149. description: '资源ID'
  150. })
  151. }),
  152. body: {
  153. content: {
  154. 'application/json': { schema: updateSchema }
  155. }
  156. }
  157. },
  158. responses: {
  159. 200: {
  160. description: '更新成功',
  161. content: { 'application/json': { schema: getSchema } }
  162. },
  163. 400: {
  164. description: '无效输入',
  165. content: { 'application/json': { schema: ErrorSchema } }
  166. },
  167. 404: {
  168. description: '资源不存在',
  169. content: { 'application/json': { schema: ErrorSchema } }
  170. },
  171. 500: {
  172. description: '服务器错误',
  173. content: { 'application/json': { schema: ErrorSchema } }
  174. }
  175. }
  176. });
  177. // 删除资源路由
  178. const deleteRouteDef = createRoute({
  179. method: 'delete',
  180. path: '/{id}',
  181. middleware,
  182. request: {
  183. params: z.object({
  184. id: z.coerce.number<number>().openapi({
  185. param: { name: 'id', in: 'path' },
  186. example: 1,
  187. description: '资源ID'
  188. })
  189. })
  190. },
  191. responses: {
  192. 204: { description: '删除成功' },
  193. 404: {
  194. description: '资源不存在',
  195. content: { 'application/json': { schema: ErrorSchema } }
  196. },
  197. 500: {
  198. description: '服务器错误',
  199. content: { 'application/json': { schema: ErrorSchema } }
  200. }
  201. }
  202. });
  203. // 注册路由处理函数
  204. // 只读模式下只注册 GET 路由
  205. if (!readOnly) {
  206. // 完整 CRUD 路由
  207. const routes = app
  208. .openapi(listRoute, async (c) => {
  209. try {
  210. const query = c.req.valid('query') as any;
  211. const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
  212. // 构建排序对象
  213. const order: any = {};
  214. if (sortBy) {
  215. order[sortBy] = sortOrder || 'DESC';
  216. } else {
  217. order['id'] = 'DESC';
  218. }
  219. // 解析筛选条件
  220. let parsedFilters: any = undefined;
  221. if (filters) {
  222. try {
  223. parsedFilters = JSON.parse(filters);
  224. } catch (e) {
  225. return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
  226. }
  227. }
  228. const crudService = new ConcreteCrudService(entity, {
  229. userTracking: userTracking,
  230. relationFields: relationFields
  231. });
  232. const [data, total] = await crudService.getList(
  233. page,
  234. pageSize,
  235. keyword,
  236. searchFields,
  237. undefined,
  238. relations || [],
  239. order,
  240. parsedFilters
  241. );
  242. return c.json({
  243. // data: z.array(listSchema).parse(data),
  244. data: await parseWithAwait(z.array(listSchema), data),
  245. pagination: { total, current: page, pageSize }
  246. }, 200);
  247. } catch (error) {
  248. if (error instanceof z.ZodError) {
  249. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  250. }
  251. return c.json({
  252. code: 500,
  253. message: error instanceof Error ? error.message : '获取列表失败'
  254. }, 500);
  255. }
  256. })
  257. // @ts-ignore
  258. .openapi(createRouteDef, async (c: any) => {
  259. try {
  260. const data = c.req.valid('json');
  261. const user = c.get('user');
  262. const crudService = new ConcreteCrudService(entity, {
  263. userTracking: userTracking,
  264. relationFields: relationFields
  265. });
  266. const result = await crudService.create(data, user?.id);
  267. return c.json(result, 201);
  268. } catch (error) {
  269. if (error instanceof z.ZodError) {
  270. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  271. }
  272. return c.json({
  273. code: 500,
  274. message: error instanceof Error ? error.message : '创建资源失败'
  275. }, 500);
  276. }
  277. })
  278. // @ts-ignore
  279. .openapi(getRouteDef, async (c: any) => {
  280. try {
  281. const { id } = c.req.valid('param');
  282. const crudService = new ConcreteCrudService(entity, {
  283. userTracking: userTracking,
  284. relationFields: relationFields
  285. });
  286. const result = await crudService.getById(id, relations || []);
  287. if (!result) {
  288. return c.json({ code: 404, message: '资源不存在' }, 404);
  289. }
  290. // return c.json(await getSchema.parseAsync(result), 200);
  291. return c.json(await parseWithAwait(getSchema, result), 200);
  292. } catch (error) {
  293. if (error instanceof z.ZodError) {
  294. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  295. }
  296. return c.json({
  297. code: 500,
  298. message: error instanceof Error ? error.message : '获取资源失败'
  299. }, 500);
  300. }
  301. })
  302. // @ts-ignore
  303. .openapi(updateRouteDef, async (c: any) => {
  304. try {
  305. const { id } = c.req.valid('param');
  306. const data = c.req.valid('json');
  307. const user = c.get('user');
  308. const crudService = new ConcreteCrudService(entity, {
  309. userTracking: userTracking,
  310. relationFields: relationFields
  311. });
  312. const result = await crudService.update(id, data, user?.id);
  313. if (!result) {
  314. return c.json({ code: 404, message: '资源不存在' }, 404);
  315. }
  316. return c.json(result, 200);
  317. } catch (error) {
  318. if (error instanceof z.ZodError) {
  319. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  320. }
  321. return c.json({
  322. code: 500,
  323. message: error instanceof Error ? error.message : '更新资源失败'
  324. }, 500);
  325. }
  326. })
  327. .openapi(deleteRouteDef, async (c: any) => {
  328. try {
  329. const { id } = c.req.valid('param');
  330. const crudService = new ConcreteCrudService(entity, {
  331. userTracking: userTracking,
  332. relationFields: relationFields
  333. });
  334. const success = await crudService.delete(id);
  335. if (!success) {
  336. return c.json({ code: 404, message: '资源不存在' }, 404);
  337. }
  338. return c.body(null, 204);
  339. } catch (error) {
  340. if (error instanceof z.ZodError) {
  341. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  342. }
  343. return c.json({
  344. code: 500,
  345. message: error instanceof Error ? error.message : '删除资源失败'
  346. }, 500);
  347. }
  348. });
  349. return routes;
  350. } else {
  351. // 只读模式,只注册 GET 路由
  352. const routes = app
  353. .openapi(listRoute, async (c) => {
  354. try {
  355. const query = c.req.valid('query') as any;
  356. const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
  357. // 构建排序对象
  358. const order: any = {};
  359. if (sortBy) {
  360. order[sortBy] = sortOrder || 'DESC';
  361. } else {
  362. order['id'] = 'DESC';
  363. }
  364. // 解析筛选条件
  365. let parsedFilters: any = undefined;
  366. if (filters) {
  367. try {
  368. parsedFilters = JSON.parse(filters);
  369. } catch (e) {
  370. return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
  371. }
  372. }
  373. const crudService = new ConcreteCrudService(entity, {
  374. userTracking: userTracking,
  375. relationFields: relationFields
  376. });
  377. const [data, total] = await crudService.getList(
  378. page,
  379. pageSize,
  380. keyword,
  381. searchFields,
  382. undefined,
  383. relations || [],
  384. order,
  385. parsedFilters
  386. );
  387. return c.json({
  388. data: await parseWithAwait(z.array(listSchema), data),
  389. pagination: { total, current: page, pageSize }
  390. }, 200);
  391. } catch (error) {
  392. if (error instanceof z.ZodError) {
  393. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  394. }
  395. return c.json({
  396. code: 500,
  397. message: error instanceof Error ? error.message : '获取列表失败'
  398. }, 500);
  399. }
  400. })
  401. // @ts-ignore
  402. .openapi(getRouteDef, async (c: any) => {
  403. try {
  404. const { id } = c.req.valid('param');
  405. const crudService = new ConcreteCrudService(entity, {
  406. userTracking: userTracking,
  407. relationFields: relationFields
  408. });
  409. const result = await crudService.getById(id, relations || []);
  410. if (!result) {
  411. return c.json({ code: 404, message: '资源不存在' }, 404);
  412. }
  413. return c.json(await parseWithAwait(getSchema, result), 200);
  414. } catch (error) {
  415. if (error instanceof z.ZodError) {
  416. return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
  417. }
  418. return c.json({
  419. code: 500,
  420. message: error instanceof Error ? error.message : '获取资源失败'
  421. }, 500);
  422. }
  423. });
  424. return routes;
  425. }
  426. }