371 lines
11 KiB
Python
371 lines
11 KiB
Python
"""Project database catalog and registered-table CRUD APIs."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, File, HTTPException, Query, Request, Response, UploadFile
|
|
|
|
from app.auth import AdminUser, CurrentUser, DataViewerUser
|
|
from app.data_platform.mysql_service import (
|
|
IDENTIFIER_PATTERN,
|
|
MAX_CSV_BYTES,
|
|
create_custom_table,
|
|
create_custom_table_from_sql,
|
|
create_record,
|
|
create_table_column,
|
|
delete_custom_table,
|
|
delete_project_database,
|
|
delete_record,
|
|
delete_table_column,
|
|
ensure_project_database,
|
|
execute_project_sql,
|
|
export_csv_records,
|
|
import_csv_records,
|
|
inspect_table,
|
|
list_admin_action_logs,
|
|
list_databases,
|
|
list_records,
|
|
list_tables,
|
|
preview_csv_import,
|
|
rename_custom_table,
|
|
rename_project_database,
|
|
update_record,
|
|
update_table_column,
|
|
)
|
|
|
|
router = APIRouter(prefix="/data-platform")
|
|
|
|
|
|
@router.get("/databases")
|
|
async def databases(_user: DataViewerUser):
|
|
return await list_databases()
|
|
|
|
|
|
@router.post("/databases")
|
|
async def add_database(body: dict[str, Any], user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以创建关系数据库")
|
|
database_id = str(body.get("database_id") or body.get("project_id") or "").strip()
|
|
if not IDENTIFIER_PATTERN.fullmatch(database_id):
|
|
raise HTTPException(
|
|
400,
|
|
"数据库编码必须以小写字母开头,只能包含小写字母、数字和下划线,长度为 2–63 位",
|
|
)
|
|
display_name = str(body.get("display_name") or database_id).strip()
|
|
tenant_id = str(body.get("tenant_id") or database_id).strip()
|
|
try:
|
|
return await ensure_project_database(database_id, tenant_id, display_name)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.post("/databases/initialize")
|
|
async def initialize_databases(user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以初始化关系数据库")
|
|
raise HTTPException(410, "图谱项目不再自动初始化关系数据库,请在数据中心独立创建")
|
|
|
|
|
|
@router.patch("/databases/{project_id}")
|
|
async def edit_database(project_id: str, body: dict[str, Any], user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以修改关系数据库")
|
|
try:
|
|
return await rename_project_database(
|
|
project_id,
|
|
str(body.get("display_name") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.delete("/databases/{project_id}")
|
|
async def remove_database(project_id: str, body: dict[str, Any], user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以删除关系数据库")
|
|
try:
|
|
return await delete_project_database(
|
|
project_id,
|
|
str(body.get("confirm_name") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.get("/databases/{project_id}/tables")
|
|
async def tables(project_id: str, _user: DataViewerUser):
|
|
return await list_tables(project_id)
|
|
|
|
|
|
@router.post("/databases/{project_id}/console/execute")
|
|
async def execute_console_sql(
|
|
project_id: str,
|
|
body: dict[str, Any],
|
|
request: Request,
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以使用 SQL 控制台")
|
|
try:
|
|
return await execute_project_sql(
|
|
project_id,
|
|
str(body.get("sql") or ""),
|
|
actor=user["username"],
|
|
source_ip=request.client.host if request.client else None,
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.get("/security/audit-logs")
|
|
async def security_audit_logs(
|
|
_user: AdminUser,
|
|
limit: int = Query(default=200, ge=1, le=1000),
|
|
):
|
|
return await list_admin_action_logs(limit)
|
|
|
|
|
|
@router.get("/databases/{project_id}/tables/{table_code}/inspection")
|
|
async def table_inspection(project_id: str, table_code: str, _user: DataViewerUser):
|
|
return await inspect_table(project_id, table_code)
|
|
|
|
|
|
@router.post("/databases/{project_id}/tables/{table_code}/columns")
|
|
async def add_table_column(
|
|
project_id: str,
|
|
table_code: str,
|
|
body: dict[str, Any],
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以新增字段")
|
|
try:
|
|
await create_table_column(project_id, table_code, body)
|
|
return await inspect_table(project_id, table_code)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.patch(
|
|
"/databases/{project_id}/tables/{table_code}/columns/{column_code}"
|
|
)
|
|
async def edit_table_column(
|
|
project_id: str,
|
|
table_code: str,
|
|
column_code: str,
|
|
body: dict[str, Any],
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以修改字段")
|
|
try:
|
|
await update_table_column(project_id, table_code, column_code, body)
|
|
return await inspect_table(project_id, table_code)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.delete(
|
|
"/databases/{project_id}/tables/{table_code}/columns/{column_code}"
|
|
)
|
|
async def remove_table_column(
|
|
project_id: str,
|
|
table_code: str,
|
|
column_code: str,
|
|
body: dict[str, Any],
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以删除字段")
|
|
try:
|
|
await delete_table_column(
|
|
project_id,
|
|
table_code,
|
|
column_code,
|
|
str(body.get("confirm_name") or ""),
|
|
)
|
|
return await inspect_table(project_id, table_code)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.post("/databases/{project_id}/tables")
|
|
async def add_table(project_id: str, body: dict[str, Any], user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以创建数据表")
|
|
try:
|
|
definition = await create_custom_table(project_id, body, user["username"])
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
return definition.as_dict()
|
|
|
|
|
|
@router.post("/databases/{project_id}/tables/from-sql")
|
|
async def add_table_from_sql(project_id: str, body: dict[str, Any], user: CurrentUser):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以通过 SQL 创建数据表")
|
|
try:
|
|
definition = await create_custom_table_from_sql(
|
|
project_id,
|
|
body,
|
|
user["username"],
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
return definition.as_dict()
|
|
|
|
|
|
@router.patch("/databases/{project_id}/tables/{table_code}")
|
|
async def edit_table(
|
|
project_id: str,
|
|
table_code: str,
|
|
body: dict[str, Any],
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以修改数据表")
|
|
try:
|
|
definition = await rename_custom_table(
|
|
project_id,
|
|
table_code,
|
|
str(body.get("code") or table_code),
|
|
str(body.get("label") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
return definition.as_dict()
|
|
|
|
|
|
@router.delete("/databases/{project_id}/tables/{table_code}")
|
|
async def remove_table(
|
|
project_id: str,
|
|
table_code: str,
|
|
body: dict[str, Any],
|
|
user: CurrentUser,
|
|
):
|
|
if "admin" not in user.get("roles", []):
|
|
raise HTTPException(403, "只有系统管理员可以删除数据表")
|
|
try:
|
|
return await delete_custom_table(
|
|
project_id,
|
|
table_code,
|
|
str(body.get("confirm_name") or ""),
|
|
)
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc)) from exc
|
|
|
|
|
|
@router.get("/databases/{project_id}/tables/{table_code}/records")
|
|
async def records(
|
|
project_id: str,
|
|
table_code: str,
|
|
_user: DataViewerUser,
|
|
page: int = Query(default=1, ge=1),
|
|
page_size: int = Query(default=50, ge=1, le=5000),
|
|
search: str | None = None,
|
|
sort_field: str | None = None,
|
|
sort_order: str = Query(default="desc", pattern="^(asc|desc)$"),
|
|
):
|
|
return await list_records(
|
|
project_id,
|
|
table_code,
|
|
page=page,
|
|
page_size=page_size,
|
|
search=search,
|
|
sort_field=sort_field,
|
|
sort_order=sort_order,
|
|
)
|
|
|
|
|
|
async def _read_csv_upload(file: UploadFile) -> tuple[bytes, str]:
|
|
file_name = str(file.filename or "data.csv").strip()
|
|
if not file_name.lower().endswith(".csv"):
|
|
raise HTTPException(400, "仅支持 .csv 文件")
|
|
content = await file.read(MAX_CSV_BYTES + 1)
|
|
if len(content) > MAX_CSV_BYTES:
|
|
raise HTTPException(413, "CSV 文件不能超过 50 MB")
|
|
return content, file_name
|
|
|
|
|
|
@router.post(
|
|
"/databases/{project_id}/tables/{table_code}/records/import/preview"
|
|
)
|
|
async def preview_records_import(
|
|
project_id: str,
|
|
table_code: str,
|
|
user: AdminUser,
|
|
file: UploadFile = File(...),
|
|
):
|
|
content, file_name = await _read_csv_upload(file)
|
|
return await preview_csv_import(project_id, table_code, content, file_name)
|
|
|
|
|
|
@router.post("/databases/{project_id}/tables/{table_code}/records/import")
|
|
async def import_records(
|
|
project_id: str,
|
|
table_code: str,
|
|
user: AdminUser,
|
|
file: UploadFile = File(...),
|
|
):
|
|
content, file_name = await _read_csv_upload(file)
|
|
return await import_csv_records(
|
|
project_id,
|
|
table_code,
|
|
content,
|
|
file_name,
|
|
user["username"],
|
|
)
|
|
|
|
|
|
@router.get("/databases/{project_id}/tables/{table_code}/records/export")
|
|
async def export_records(
|
|
project_id: str,
|
|
table_code: str,
|
|
_user: DataViewerUser,
|
|
search: str | None = None,
|
|
):
|
|
content, filename, total = await export_csv_records(
|
|
project_id,
|
|
table_code,
|
|
search,
|
|
)
|
|
return Response(
|
|
content=content,
|
|
media_type="text/csv; charset=utf-8",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
"X-Exported-Count": str(total),
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/databases/{project_id}/tables/{table_code}/records")
|
|
async def add_record(
|
|
project_id: str,
|
|
table_code: str,
|
|
body: dict[str, Any],
|
|
user: AdminUser,
|
|
):
|
|
return await create_record(project_id, table_code, body, user["username"])
|
|
|
|
|
|
@router.patch("/databases/{project_id}/tables/{table_code}/records/{record_id}")
|
|
async def edit_record(
|
|
project_id: str,
|
|
table_code: str,
|
|
record_id: str,
|
|
body: dict[str, Any],
|
|
user: AdminUser,
|
|
):
|
|
return await update_record(project_id, table_code, record_id, body, user["username"])
|
|
|
|
|
|
@router.delete("/databases/{project_id}/tables/{table_code}/records/{record_id}")
|
|
async def remove_record(
|
|
project_id: str,
|
|
table_code: str,
|
|
record_id: str,
|
|
user: AdminUser,
|
|
):
|
|
return await delete_record(project_id, table_code, record_id, user["username"])
|