generic-crud.routes.ts 14 KB

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