|
|
@@ -1,11 +1,13 @@
|
|
|
import { createRoute, OpenAPIHono, extendZodWithOpenApi } from '@hono/zod-openapi';
|
|
|
-import { z } from '@hono/zod-openapi';
|
|
|
+import { z } from 'zod';
|
|
|
+import type { ZodError } from 'zod';
|
|
|
import { ObjectLiteral } from 'typeorm';
|
|
|
import { CrudOptions } from '../services/generic-crud.service';
|
|
|
import { ErrorSchema } from '@d8d/shared-utils';
|
|
|
import { AuthContext } from '@d8d/shared-types';
|
|
|
import { parseWithAwait } from '@d8d/shared-utils';
|
|
|
import { ConcreteCrudService } from '../services/concrete-crud.service';
|
|
|
+import { PermissionError } from '../types/data-permission.types';
|
|
|
|
|
|
extendZodWithOpenApi(z)
|
|
|
|
|
|
@@ -18,7 +20,7 @@ export function createCrudRoutes<
|
|
|
>(
|
|
|
options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>
|
|
|
) {
|
|
|
- const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
|
|
|
+ const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false, dataPermission, defaultFilters, tenantOptions } = options;
|
|
|
|
|
|
// 创建路由实例
|
|
|
const app = new OpenAPIHono<AuthContext>();
|
|
|
@@ -30,11 +32,11 @@ export function createCrudRoutes<
|
|
|
middleware,
|
|
|
request: {
|
|
|
query: z.object({
|
|
|
- page: z.coerce.number<number>().int().positive().default(1).openapi({
|
|
|
+ page: z.coerce.number<number>().int().positive('页码必须是正整数').default(1).openapi({
|
|
|
example: 1,
|
|
|
description: '页码,从1开始'
|
|
|
}),
|
|
|
- pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
|
|
|
+ pageSize: z.coerce.number<number>().int().positive('每页数量必须是正整数').default(10).openapi({
|
|
|
example: 10,
|
|
|
description: '每页数量'
|
|
|
}),
|
|
|
@@ -81,6 +83,10 @@ export function createCrudRoutes<
|
|
|
description: '认证失败',
|
|
|
content: { 'application/json': { schema: ErrorSchema } }
|
|
|
},
|
|
|
+ 403: {
|
|
|
+ description: '权限不足',
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
+ },
|
|
|
500: {
|
|
|
description: '服务器错误',
|
|
|
content: { 'application/json': { schema: ErrorSchema } }
|
|
|
@@ -113,6 +119,10 @@ export function createCrudRoutes<
|
|
|
description: '认证失败',
|
|
|
content: { 'application/json': { schema: ErrorSchema } }
|
|
|
},
|
|
|
+ 403: {
|
|
|
+ description: '权限不足',
|
|
|
+ content: { 'application/json': { schema: ErrorSchema } }
|
|
|
+ },
|
|
|
500: {
|
|
|
description: '服务器错误',
|
|
|
content: { 'application/json': { schema: ErrorSchema } }
|
|
|
@@ -242,6 +252,7 @@ export function createCrudRoutes<
|
|
|
try {
|
|
|
const query = c.req.valid('query') as any;
|
|
|
const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
|
|
|
+ const user = c.get('user');
|
|
|
|
|
|
// 构建排序对象
|
|
|
const order: any = {};
|
|
|
@@ -252,19 +263,36 @@ export function createCrudRoutes<
|
|
|
}
|
|
|
|
|
|
// 解析筛选条件
|
|
|
- let parsedFilters: any = undefined;
|
|
|
+ let parsedFilters: any = { ...defaultFilters };
|
|
|
if (filters) {
|
|
|
try {
|
|
|
- parsedFilters = JSON.parse(filters);
|
|
|
+ const userFilters = JSON.parse(filters);
|
|
|
+ // 合并默认过滤条件和用户传入的过滤条件
|
|
|
+ parsedFilters = { ...parsedFilters, ...userFilters };
|
|
|
} catch (e) {
|
|
|
return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
|
|
|
}
|
|
|
}
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
+
|
|
|
const [data, total] = await crudService.getList(
|
|
|
page,
|
|
|
pageSize,
|
|
|
@@ -273,17 +301,32 @@ export function createCrudRoutes<
|
|
|
undefined,
|
|
|
relations || [],
|
|
|
order,
|
|
|
- parsedFilters
|
|
|
+ parsedFilters,
|
|
|
+ user?.id
|
|
|
);
|
|
|
|
|
|
- return c.json({
|
|
|
- // data: z.array(listSchema).parse(data),
|
|
|
- data: await parseWithAwait(z.array(listSchema), data),
|
|
|
- pagination: { total, current: page, pageSize }
|
|
|
- }, 200);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const validatedData = await parseWithAwait(z.array(listSchema), data);
|
|
|
+ return c.json({
|
|
|
+ data: validatedData,
|
|
|
+ pagination: { total, current: page, pageSize }
|
|
|
+ }, 200);
|
|
|
+ } catch (validationError) {
|
|
|
+ if (validationError instanceof z.ZodError) {
|
|
|
+ const zodError = validationError as ZodError;
|
|
|
+ return c.json({
|
|
|
+ code: 400,
|
|
|
+ message: '参数验证失败',
|
|
|
+ errors: (zodError as any).errors || validationError.message
|
|
|
+ }, 400);
|
|
|
+ }
|
|
|
+ throw validationError;
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
- return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
}
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
@@ -299,14 +342,46 @@ export function createCrudRoutes<
|
|
|
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
+
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
const result = await crudService.create(data, user?.id);
|
|
|
- return c.json(result, 201);
|
|
|
+ // 重新获取包含关联关系的数据
|
|
|
+ const fullResult = await crudService.getById(result.id, relations || [], user?.id);
|
|
|
+ return c.json(await parseWithAwait(getSchema, fullResult), 201);
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
- return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理数据库唯一约束错误
|
|
|
+ if (error instanceof Error && error.message.includes('duplicate key value violates unique constraint')) {
|
|
|
+ return c.json({ code: 400, message: '数据已存在,请检查唯一性约束' }, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理权限错误,返回403状态码
|
|
|
+ if (error instanceof Error && error.message.includes('无权')) {
|
|
|
+ return c.json({
|
|
|
+ code: 403,
|
|
|
+ message: error.message
|
|
|
+ }, 403);
|
|
|
}
|
|
|
+
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
message: error instanceof Error ? error.message : '创建资源失败'
|
|
|
@@ -317,22 +392,53 @@ export function createCrudRoutes<
|
|
|
.openapi(getRouteDef, async (c: any) => {
|
|
|
try {
|
|
|
const { id } = c.req.valid('param');
|
|
|
+ const user = c.get('user');
|
|
|
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
- const result = await crudService.getById(id, relations || []);
|
|
|
+
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
+ const result = await crudService.getById(id, relations || [], user?.id);
|
|
|
|
|
|
if (!result) {
|
|
|
return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
}
|
|
|
|
|
|
+ // 应用默认过滤条件
|
|
|
+ if (defaultFilters && Object.keys(defaultFilters).length > 0) {
|
|
|
+ const shouldFilter = Object.entries(defaultFilters).some(([key, value]) => {
|
|
|
+ return result[key as keyof T] !== value;
|
|
|
+ });
|
|
|
+ if (shouldFilter) {
|
|
|
+ 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);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
+ }
|
|
|
+ if (error instanceof PermissionError) {
|
|
|
+ // GET操作中,权限错误应该返回404而不是403
|
|
|
+ return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
}
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
@@ -349,19 +455,46 @@ export function createCrudRoutes<
|
|
|
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
+
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
const result = await crudService.update(id, data, user?.id);
|
|
|
|
|
|
if (!result) {
|
|
|
return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
}
|
|
|
|
|
|
- return c.json(result, 200);
|
|
|
+ // 重新获取包含关联关系的数据
|
|
|
+ const fullResult = await crudService.getById(id, relations || [], user?.id);
|
|
|
+ return c.json(await parseWithAwait(getSchema, fullResult), 200);
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
- return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理权限错误,返回403状态码
|
|
|
+ if (error instanceof Error && error.message.includes('无权')) {
|
|
|
+ return c.json({
|
|
|
+ code: 403,
|
|
|
+ message: error.message
|
|
|
+ }, 403);
|
|
|
}
|
|
|
+
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
message: error instanceof Error ? error.message : '更新资源失败'
|
|
|
@@ -371,12 +504,28 @@ export function createCrudRoutes<
|
|
|
.openapi(deleteRouteDef, async (c: any) => {
|
|
|
try {
|
|
|
const { id } = c.req.valid('param');
|
|
|
+ const user = c.get('user');
|
|
|
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
- const success = await crudService.delete(id);
|
|
|
+
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
+ const success = await crudService.delete(id, user?.id);
|
|
|
|
|
|
if (!success) {
|
|
|
return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
@@ -385,8 +534,18 @@ export function createCrudRoutes<
|
|
|
return c.body(null, 204);
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
- return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理权限错误,返回403状态码
|
|
|
+ if (error instanceof Error && error.message.includes('无权')) {
|
|
|
+ return c.json({
|
|
|
+ code: 403,
|
|
|
+ message: error.message
|
|
|
+ }, 403);
|
|
|
}
|
|
|
+
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
message: error instanceof Error ? error.message : '删除资源失败'
|
|
|
@@ -402,6 +561,7 @@ export function createCrudRoutes<
|
|
|
try {
|
|
|
const query = c.req.valid('query') as any;
|
|
|
const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
|
|
|
+ const user = c.get('user');
|
|
|
|
|
|
// 构建排序对象
|
|
|
const order: any = {};
|
|
|
@@ -412,19 +572,36 @@ export function createCrudRoutes<
|
|
|
}
|
|
|
|
|
|
// 解析筛选条件
|
|
|
- let parsedFilters: any = undefined;
|
|
|
+ let parsedFilters: any = { ...defaultFilters };
|
|
|
if (filters) {
|
|
|
try {
|
|
|
- parsedFilters = JSON.parse(filters);
|
|
|
+ const userFilters = JSON.parse(filters);
|
|
|
+ // 合并默认过滤条件和用户传入的过滤条件
|
|
|
+ parsedFilters = { ...parsedFilters, ...userFilters };
|
|
|
} catch (e) {
|
|
|
return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
|
|
|
}
|
|
|
}
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
+
|
|
|
const [data, total] = await crudService.getList(
|
|
|
page,
|
|
|
pageSize,
|
|
|
@@ -433,7 +610,8 @@ export function createCrudRoutes<
|
|
|
undefined,
|
|
|
relations || [],
|
|
|
order,
|
|
|
- parsedFilters
|
|
|
+ parsedFilters,
|
|
|
+ user?.id
|
|
|
);
|
|
|
|
|
|
return c.json({
|
|
|
@@ -442,8 +620,18 @@ export function createCrudRoutes<
|
|
|
}, 200);
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
- return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理权限错误,返回403状态码
|
|
|
+ if (error instanceof Error && error.message.includes('无权')) {
|
|
|
+ return c.json({
|
|
|
+ code: 403,
|
|
|
+ message: error.message
|
|
|
+ }, 403);
|
|
|
}
|
|
|
+
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
message: error instanceof Error ? error.message : '获取列表失败'
|
|
|
@@ -454,22 +642,61 @@ export function createCrudRoutes<
|
|
|
.openapi(getRouteDef, async (c: any) => {
|
|
|
try {
|
|
|
const { id } = c.req.valid('param');
|
|
|
+ const user = c.get('user');
|
|
|
|
|
|
const crudService = new ConcreteCrudService(entity, {
|
|
|
userTracking: userTracking,
|
|
|
- relationFields: relationFields
|
|
|
+ relationFields: relationFields,
|
|
|
+ dataPermission: dataPermission,
|
|
|
+ tenantOptions: tenantOptions
|
|
|
});
|
|
|
- const result = await crudService.getById(id, relations || []);
|
|
|
+
|
|
|
+ // 设置租户上下文
|
|
|
+ const tenantId = c.get('tenantId');
|
|
|
+
|
|
|
+ // 优先从tenantId上下文获取,如果没有则从用户对象中提取
|
|
|
+ let finalTenantId = tenantId;
|
|
|
+ if (finalTenantId === undefined && user?.tenantId !== undefined) {
|
|
|
+ finalTenantId = user.tenantId;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (finalTenantId !== undefined) {
|
|
|
+ crudService.setTenantContext(finalTenantId);
|
|
|
+ }
|
|
|
+ const result = await crudService.getById(id, relations || [], user?.id);
|
|
|
|
|
|
if (!result) {
|
|
|
return c.json({ code: 404, message: '资源不存在' }, 404);
|
|
|
}
|
|
|
|
|
|
+ // 应用默认过滤条件
|
|
|
+ if (defaultFilters && Object.keys(defaultFilters).length > 0) {
|
|
|
+ const shouldFilter = Object.entries(defaultFilters).some(([key, value]) => {
|
|
|
+ return result[key as keyof T] !== value;
|
|
|
+ });
|
|
|
+ if (shouldFilter) {
|
|
|
+ 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);
|
|
|
+ const zodError = error as ZodError;
|
|
|
+ return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
|
|
|
}
|
|
|
+ if (error instanceof PermissionError) {
|
|
|
+ return c.json({ code: 403, message: error.message }, 403);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理权限错误,返回403状态码
|
|
|
+ if (error instanceof Error && error.message.includes('无权')) {
|
|
|
+ return c.json({
|
|
|
+ code: 403,
|
|
|
+ message: error.message
|
|
|
+ }, 403);
|
|
|
+ }
|
|
|
+
|
|
|
return c.json({
|
|
|
code: 500,
|
|
|
message: error instanceof Error ? error.message : '获取资源失败'
|