feat: expand graph lifecycle and data platform
This commit is contained in:
1 parent
00af885b1a
commit
15368f2779
50 files changed
+17461
-1040
No files matched your search
+272
-7
@@ -3,18 +3,38 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, File, HTTPException, Query, Response, UploadFile
|
||||
|
||||
from app.auth import CurrentUser
|
||||
from app.data_platform.csv_service import (
|
||||
MAX_CSV_BYTES,
|
||||
export_csv_records,
|
||||
import_csv_records,
|
||||
preview_csv_import,
|
||||
)
|
||||
from app.data_platform.record_service import (
|
||||
create_record,
|
||||
delete_record,
|
||||
inspect_table,
|
||||
list_databases,
|
||||
list_records,
|
||||
list_tables,
|
||||
update_record,
|
||||
)
|
||||
from app.data_platform.schema import ensure_all_project_databases
|
||||
from app.data_platform.schema import (
|
||||
IDENTIFIER_PATTERN,
|
||||
create_custom_table,
|
||||
create_custom_table_from_sql,
|
||||
create_table_column,
|
||||
delete_custom_table,
|
||||
delete_project_database,
|
||||
delete_table_column,
|
||||
ensure_project_database,
|
||||
rename_custom_table,
|
||||
rename_project_database,
|
||||
update_table_column,
|
||||
)
|
||||
from app.data_platform.sql_console import execute_project_sql
|
||||
|
||||
router = APIRouter(prefix="/data-platform")
|
||||
|
||||
@@ -24,12 +44,52 @@ async def databases(_user: CurrentUser):
|
||||
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()
|
||||
return await ensure_project_database(database_id, tenant_id, display_name)
|
||||
|
||||
|
||||
@router.post("/databases/initialize")
|
||||
async def initialize_databases(user: CurrentUser):
|
||||
if "admin" not in user.get("roles", []):
|
||||
raise HTTPException(403, "只有系统管理员可以初始化项目数据库")
|
||||
await ensure_all_project_databases()
|
||||
return {"ok": True}
|
||||
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")
|
||||
@@ -37,13 +97,157 @@ async def tables(project_id: str, _user: CurrentUser):
|
||||
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],
|
||||
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 ""))
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/databases/{project_id}/tables/{table_code}/inspection")
|
||||
async def table_inspection(project_id: str, table_code: str, _user: CurrentUser):
|
||||
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: CurrentUser,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
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)$"),
|
||||
@@ -59,6 +263,68 @@ async def records(
|
||||
)
|
||||
|
||||
|
||||
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: CurrentUser,
|
||||
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: CurrentUser,
|
||||
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: CurrentUser,
|
||||
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,
|
||||
@@ -88,4 +354,3 @@ async def remove_record(
|
||||
user: CurrentUser,
|
||||
):
|
||||
return await delete_record(project_id, table_code, record_id, user["username"])
|
||||
|
||||
Reference in new issue
Block a user