generic-crud.routes.ts 14 KB

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