generic-crud.routes.ts 14 KB

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