generic-crud.routes.ts 16 KB

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