|
@@ -0,0 +1,463 @@
|
|
|
|
|
+import { createRoute, OpenAPIHono, extendZodWithOpenApi } from '@hono/zod-openapi';
|
|
|
|
|
+import { z } from '@hono/zod-openapi';
|
|
|
|
|
+import { CrudOptions } from '../services/generic-crud.service';
|
|
|
|
|
+import { ErrorSchema } from '@d8d/shared-utils';
|
|
|
|
|
+import { AuthContext } from '@d8d/shared-types';
|
|
|
|
|
+import { ObjectLiteral } from 'typeorm';
|
|
|
|
|
+import { parseWithAwait } from '@d8d/shared-utils';
|
|
|
|
|
+import { ConcreteCrudService } from '../services/concrete-crud.service';
|
|
|
|
|
+
|
|
|
|
|
+extendZodWithOpenApi(z)
|
|
|
|
|
+
|
|
|
|
|
+export function createCrudRoutes<
|
|
|
|
|
+ T extends ObjectLiteral,
|
|
|
|
|
+ CreateSchema extends z.ZodSchema = z.ZodSchema,
|
|
|
|
|
+ UpdateSchema extends z.ZodSchema = z.ZodSchema,
|
|
|
|
|
+ GetSchema extends z.ZodSchema = z.ZodSchema,
|
|
|
|
|
+ ListSchema extends z.ZodSchema = z.ZodSchema
|
|
|
|
|
+>(
|
|
|
|
|
+ dataSource: DataSource,
|
|
|
|
|
+ options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>
|
|
|
|
|
+) {
|
|
|
|
|
+ const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建路由实例
|
|
|
|
|
+ const app = new OpenAPIHono<AuthContext>();
|
|
|
|
|
+
|
|
|
|
|
+ // 分页查询路由
|
|
|
|
|
+ const listRoute = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/',
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ request: {
|
|
|
|
|
+ query: z.object({
|
|
|
|
|
+ page: z.coerce.number<number>().int().positive().default(1).openapi({
|
|
|
|
|
+ example: 1,
|
|
|
|
|
+ description: '页码,从1开始'
|
|
|
|
|
+ }),
|
|
|
|
|
+ pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
|
|
|
|
|
+ example: 10,
|
|
|
|
|
+ description: '每页数量'
|
|
|
|
|
+ }),
|
|
|
|
|
+ keyword: z.string().optional().openapi({
|
|
|
|
|
+ example: '搜索关键词',
|
|
|
|
|
+ description: '搜索关键词'
|
|
|
|
|
+ }),
|
|
|
|
|
+ sortBy: z.string().optional().openapi({
|
|
|
|
|
+ example: 'createdAt',
|
|
|
|
|
+ description: '排序字段'
|
|
|
|
|
+ }),
|
|
|
|
|
+ sortOrder: z.enum(['ASC', 'DESC']).optional().default('DESC').openapi({
|
|
|
|
|
+ example: 'DESC',
|
|
|
|
|
+ description: '排序方向'
|
|
|
|
|
+ }),
|
|
|
|
|
+ // 增强的筛选参数
|
|
|
|
|
+ filters: z.string().optional().openapi({
|
|
|
|
|
+ example: '{"status": 1, "createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}',
|
|
|
|
|
+ description: '筛选条件(JSON字符串),支持精确匹配、范围查询、IN查询等'
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ 200: {
|
|
|
|
|
+ description: '成功获取列表',
|
|
|
|
|
+ content: {
|
|
|
|
|
+ 'application/json': {
|
|
|
|
|
+ schema: z.object({
|
|
|
|
|
+ data: z.array(listSchema),
|
|
|
|
|
+ pagination: z.object({
|
|
|
|
|
+ total: z.number().openapi({ example: 100, description: '总记录数' }),
|
|
|
|
|
+ current: z.number().openapi({ example: 1, description: '当前页码' }),
|
|
|
|
|
+ pageSize: z.number().openapi({ example: 10, description: '每页数量' })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ 400: {
|
|
|
|
|
+ description: '参数错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 500: {
|
|
|
|
|
+ description: '服务器错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 创建资源路由
|
|
|
|
|
+ const createRouteDef = createRoute({
|
|
|
|
|
+ method: 'post',
|
|
|
|
|
+ path: '/',
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ request: {
|
|
|
|
|
+ body: {
|
|
|
|
|
+ content: {
|
|
|
|
|
+ 'application/json': { schema: createSchema }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ 201: {
|
|
|
|
|
+ description: '创建成功',
|
|
|
|
|
+ content: { 'application/json': { schema: getSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 400: {
|
|
|
|
|
+ description: '输入数据无效',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 500: {
|
|
|
|
|
+ description: '服务器错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 获取单个资源路由
|
|
|
|
|
+ const getRouteDef = createRoute({
|
|
|
|
|
+ method: 'get',
|
|
|
|
|
+ path: '/{id}',
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ request: {
|
|
|
|
|
+ params: z.object({
|
|
|
|
|
+ id: z.coerce.number<number>().openapi({
|
|
|
|
|
+ param: { name: 'id', in: 'path' },
|
|
|
|
|
+ example: 1,
|
|
|
|
|
+ description: '资源ID'
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ 200: {
|
|
|
|
|
+ description: '成功获取详情',
|
|
|
|
|
+ content: { 'application/json': { schema: getSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 400: {
|
|
|
|
|
+ description: '资源不存在',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 404: {
|
|
|
|
|
+ description: '参数验证失败',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 500: {
|
|
|
|
|
+ description: '服务器错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 更新资源路由
|
|
|
|
|
+ const updateRouteDef = createRoute({
|
|
|
|
|
+ method: 'put',
|
|
|
|
|
+ path: '/{id}',
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ request: {
|
|
|
|
|
+ params: z.object({
|
|
|
|
|
+ id: z.coerce.number<number>().openapi({
|
|
|
|
|
+ param: { name: 'id', in: 'path' },
|
|
|
|
|
+ example: 1,
|
|
|
|
|
+ description: '资源ID'
|
|
|
|
|
+ })
|
|
|
|
|
+ }),
|
|
|
|
|
+ body: {
|
|
|
|
|
+ content: {
|
|
|
|
|
+ 'application/json': { schema: updateSchema }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ 200: {
|
|
|
|
|
+ description: '更新成功',
|
|
|
|
|
+ content: { 'application/json': { schema: getSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 400: {
|
|
|
|
|
+ description: '无效输入',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 404: {
|
|
|
|
|
+ description: '资源不存在',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 500: {
|
|
|
|
|
+ description: '服务器错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 删除资源路由
|
|
|
|
|
+ const deleteRouteDef = createRoute({
|
|
|
|
|
+ method: 'delete',
|
|
|
|
|
+ path: '/{id}',
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ request: {
|
|
|
|
|
+ params: z.object({
|
|
|
|
|
+ id: z.coerce.number<number>().openapi({
|
|
|
|
|
+ param: { name: 'id', in: 'path' },
|
|
|
|
|
+ example: 1,
|
|
|
|
|
+ description: '资源ID'
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ responses: {
|
|
|
|
|
+ 204: { description: '删除成功' },
|
|
|
|
|
+ 404: {
|
|
|
|
|
+ description: '资源不存在',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ },
|
|
|
|
|
+ 500: {
|
|
|
|
|
+ description: '服务器错误',
|
|
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 注册路由处理函数
|
|
|
|
|
+
|
|
|
|
|
+ // 只读模式下只注册 GET 路由
|
|
|
|
|
+ if (!readOnly) {
|
|
|
|
|
+ // 完整 CRUD 路由
|
|
|
|
|
+ const routes = app
|
|
|
|
|
+ .openapi(listRoute, async (c) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const query = c.req.valid('query') as any;
|
|
|
|
|
+ const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建排序对象
|
|
|
|
|
+ const order: any = {};
|
|
|
|
|
+ if (sortBy) {
|
|
|
|
|
+ order[sortBy] = sortOrder || 'DESC';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ order['id'] = 'DESC';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析筛选条件
|
|
|
|
|
+ let parsedFilters: any = undefined;
|
|
|
|
|
+ if (filters) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ parsedFilters = JSON.parse(filters);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const [data, total] = await crudService.getList(
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize,
|
|
|
|
|
+ keyword,
|
|
|
|
|
+ searchFields,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ relations || [],
|
|
|
|
|
+ order,
|
|
|
|
|
+ parsedFilters
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ // data: z.array(listSchema).parse(data),
|
|
|
|
|
+ data: await parseWithAwait(z.array(listSchema), data),
|
|
|
|
|
+ pagination: { total, current: page, pageSize }
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '获取列表失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // @ts-ignore
|
|
|
|
|
+ .openapi(createRouteDef, async (c: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = c.req.valid('json');
|
|
|
|
|
+ const user = c.get('user');
|
|
|
|
|
+
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+ const result = await crudService.create(data, user?.id);
|
|
|
|
|
+ return c.json(result, 201);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '创建资源失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // @ts-ignore
|
|
|
|
|
+ .openapi(getRouteDef, async (c: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { id } = c.req.valid('param');
|
|
|
|
|
+
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+ const result = await crudService.getById(id, relations || []);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result) {
|
|
|
|
|
+ return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // return c.json(await getSchema.parseAsync(result), 200);
|
|
|
|
|
+ return c.json(await parseWithAwait(getSchema, result), 200);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '获取资源失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // @ts-ignore
|
|
|
|
|
+ .openapi(updateRouteDef, async (c: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { id } = c.req.valid('param');
|
|
|
|
|
+ const data = c.req.valid('json');
|
|
|
|
|
+ const user = c.get('user');
|
|
|
|
|
+
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+ const result = await crudService.update(id, data, user?.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result) {
|
|
|
|
|
+ return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return c.json(result, 200);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '更新资源失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .openapi(deleteRouteDef, async (c: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { id } = c.req.valid('param');
|
|
|
|
|
+
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+ const success = await crudService.delete(id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return c.body(null, 204);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '删除资源失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return routes;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 只读模式,只注册 GET 路由
|
|
|
|
|
+ const routes = app
|
|
|
|
|
+ .openapi(listRoute, async (c) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const query = c.req.valid('query') as any;
|
|
|
|
|
+ const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
|
|
|
|
|
+
|
|
|
|
|
+ // 构建排序对象
|
|
|
|
|
+ const order: any = {};
|
|
|
|
|
+ if (sortBy) {
|
|
|
|
|
+ order[sortBy] = sortOrder || 'DESC';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ order['id'] = 'DESC';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析筛选条件
|
|
|
|
|
+ let parsedFilters: any = undefined;
|
|
|
|
|
+ if (filters) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ parsedFilters = JSON.parse(filters);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const [data, total] = await crudService.getList(
|
|
|
|
|
+ page,
|
|
|
|
|
+ pageSize,
|
|
|
|
|
+ keyword,
|
|
|
|
|
+ searchFields,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ relations || [],
|
|
|
|
|
+ order,
|
|
|
|
|
+ parsedFilters
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ data: await parseWithAwait(z.array(listSchema), data),
|
|
|
|
|
+ pagination: { total, current: page, pageSize }
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '获取列表失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ // @ts-ignore
|
|
|
|
|
+ .openapi(getRouteDef, async (c: any) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { id } = c.req.valid('param');
|
|
|
|
|
+
|
|
|
|
|
+ const crudService = new ConcreteCrudService(dataSource, entity, {
|
|
|
|
|
+ userTracking: userTracking,
|
|
|
|
|
+ relationFields: relationFields
|
|
|
|
|
+ });
|
|
|
|
|
+ const result = await crudService.getById(id, relations || []);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result) {
|
|
|
|
|
+ return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return c.json(await parseWithAwait(getSchema, result), 200);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error instanceof z.ZodError) {
|
|
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.json({
|
|
|
|
|
+ code: 500,
|
|
|
|
|
+ message: error instanceof Error ? error.message : '获取资源失败'
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return routes;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+}
|