Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from core.database_connection.api import router
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接管理 API"""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from app.config import settings
|
||||
from app.database import get_db
|
||||
from core.application.model import Application
|
||||
from core.database_connection.schema import (
|
||||
DatabaseConnectionCreate,
|
||||
DatabaseConnectionResponse,
|
||||
DatabaseConnectionSimpleOut,
|
||||
DatabaseConnectionTestRequest,
|
||||
DatabaseConnectionTestResponse,
|
||||
DatabaseConnectionUpdate,
|
||||
)
|
||||
from core.database_connection.service import DatabaseConnectionService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/database-connection", tags=["数据库连接管理"])
|
||||
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[DatabaseConnectionResponse], summary="获取连接列表")
|
||||
async def list_connections(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize"),
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId"),
|
||||
name: Optional[str] = None,
|
||||
code: Optional[str] = None,
|
||||
status: Optional[bool] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
items, total = await DatabaseConnectionService.get_list(
|
||||
db,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
application_id=application_id,
|
||||
name=name,
|
||||
code=code,
|
||||
status=status,
|
||||
)
|
||||
|
||||
app_ids = list({item.application_id for item in items if item.application_id})
|
||||
app_name_map = {}
|
||||
if app_ids:
|
||||
app_result = await db.execute(
|
||||
select(Application.id, Application.name).where(Application.id.in_(app_ids))
|
||||
)
|
||||
app_name_map = {row.id: row.name for row in app_result}
|
||||
|
||||
result_items = []
|
||||
for item in items:
|
||||
item_dict = DatabaseConnectionService.to_response_dict(item)
|
||||
item_dict["application_name"] = app_name_map.get(item.application_id, "")
|
||||
result_items.append(item_dict)
|
||||
|
||||
return PaginatedResponse(items=result_items, total=total)
|
||||
|
||||
|
||||
@router.post("", response_model=DatabaseConnectionResponse, summary="创建连接")
|
||||
async def create_connection(
|
||||
data: DatabaseConnectionCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
try:
|
||||
DatabaseConnectionService.validate_code(data.code)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
if await DatabaseConnectionService.check_code_exists(db, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {data.code}")
|
||||
|
||||
try:
|
||||
row = await DatabaseConnectionService.create(db, data.model_dump())
|
||||
await db.commit()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
return DatabaseConnectionService.to_response_dict(row)
|
||||
|
||||
|
||||
@router.get("/get/all", response_model=List[DatabaseConnectionSimpleOut], summary="获取全部启用连接")
|
||||
async def get_all_connections(
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
configs = await DatabaseConnectionService.get_manager_configs(db, application_id)
|
||||
return [
|
||||
DatabaseConnectionSimpleOut(
|
||||
code=item["db_name"],
|
||||
name=item.get("display_name") or item["name"],
|
||||
db_type=item["db_type"],
|
||||
host=item["host"],
|
||||
port=item["port"],
|
||||
database=item["database"],
|
||||
is_system=item.get("is_system", False),
|
||||
)
|
||||
for item in configs
|
||||
]
|
||||
|
||||
|
||||
@router.get("/check-code/{code}", summary="检查编码是否可用")
|
||||
async def check_code_available(code: str, db: AsyncSession = Depends(get_db)):
|
||||
if code == "default":
|
||||
return {"available": False, "message": "default is reserved"}
|
||||
exists = await DatabaseConnectionService.check_code_exists(db, code)
|
||||
return {"available": not exists}
|
||||
|
||||
|
||||
@router.post("/test", response_model=DatabaseConnectionTestResponse, summary="测试未保存连接")
|
||||
async def test_unsaved_connection(data: DatabaseConnectionTestRequest):
|
||||
try:
|
||||
result = await DatabaseConnectionService.test_config(data.model_dump())
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if not result.get("success"):
|
||||
logger.error(
|
||||
"Database connection test failed (unsaved): host=%s port=%s db_type=%s message=%s",
|
||||
data.host,
|
||||
data.port,
|
||||
data.db_type,
|
||||
result.get("message"),
|
||||
)
|
||||
return DatabaseConnectionTestResponse(**result)
|
||||
|
||||
|
||||
@router.get("/{conn_id}", response_model=DatabaseConnectionResponse, summary="获取连接详情")
|
||||
async def get_connection(conn_id: str, db: AsyncSession = Depends(get_db)):
|
||||
row = await DatabaseConnectionService.get_by_id(db, conn_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="连接不存在")
|
||||
return DatabaseConnectionService.to_response_dict(row)
|
||||
|
||||
|
||||
@router.put("/{conn_id}", response_model=DatabaseConnectionResponse, summary="更新连接")
|
||||
async def update_connection(
|
||||
conn_id: str,
|
||||
data: DatabaseConnectionUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
row = await DatabaseConnectionService.get_by_id(db, conn_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="连接不存在")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
try:
|
||||
row = await DatabaseConnectionService.update(db, row, update_data)
|
||||
await db.commit()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
return DatabaseConnectionService.to_response_dict(row)
|
||||
|
||||
|
||||
@router.delete("/{conn_id}", response_model=ResponseModel, summary="删除连接")
|
||||
async def delete_connection(conn_id: str, db: AsyncSession = Depends(get_db)):
|
||||
row = await DatabaseConnectionService.get_by_id(db, conn_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="连接不存在")
|
||||
try:
|
||||
await DatabaseConnectionService.delete(db, row)
|
||||
await db.commit()
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
@router.post("/{conn_id}/test", response_model=DatabaseConnectionTestResponse, summary="测试已保存连接")
|
||||
async def test_saved_connection(conn_id: str, db: AsyncSession = Depends(get_db)):
|
||||
row = await DatabaseConnectionService.get_by_id(db, conn_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="连接不存在")
|
||||
result = await DatabaseConnectionService.test_saved(db, row)
|
||||
if not result.get("success"):
|
||||
logger.error(
|
||||
"Database connection test failed (saved): id=%s code=%s name=%s message=%s",
|
||||
conn_id,
|
||||
row.code,
|
||||
row.name,
|
||||
result.get("message"),
|
||||
)
|
||||
return DatabaseConnectionTestResponse(**result)
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接配置模型"""
|
||||
from sqlalchemy import Boolean, Column, Index, Integer, JSON, String, Text
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class DatabaseConnection(BaseModel):
|
||||
"""外部数据库连接配置"""
|
||||
|
||||
__tablename__ = "core_database_connection"
|
||||
|
||||
application_id = Column(String(21), nullable=True, index=True, comment="所属应用ID")
|
||||
code = Column(String(50), unique=True, nullable=False, index=True, comment="连接编码")
|
||||
name = Column(String(100), nullable=False, comment="连接名称")
|
||||
db_type = Column(String(20), nullable=False, comment="数据库类型: postgresql/mysql/sqlserver/oracle")
|
||||
host = Column(String(255), nullable=False, comment="主机")
|
||||
port = Column(Integer, nullable=False, default=5432, comment="端口")
|
||||
user = Column(String(100), nullable=False, default="", comment="用户名")
|
||||
password_enc = Column(Text, default="", comment="加密密码")
|
||||
default_database = Column(String(100), nullable=False, default="", comment="默认数据库名")
|
||||
description = Column(Text, default="", comment="描述")
|
||||
status = Column(Boolean, default=True, index=True, comment="是否启用")
|
||||
is_system = Column(Boolean, default=False, comment="是否系统内置连接")
|
||||
extra_options = Column(JSON, default=dict, comment="扩展选项")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_core_database_connection_app_status", "application_id", "status"),
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接解析器"""
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database import AsyncSessionLocal
|
||||
from core.database_connection.model import DatabaseConnection
|
||||
from core.database_connection.types import ConnectionInfo
|
||||
from core.database_manager.service import parse_database_url
|
||||
from utils.secret_crypto import decrypt_secret
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CACHE: Dict[str, Tuple[ConnectionInfo, float]] = {}
|
||||
_CACHE_TTL = 60.0
|
||||
|
||||
|
||||
class ConnectionResolver:
|
||||
@staticmethod
|
||||
def invalidate_cache(code: Optional[str] = None) -> None:
|
||||
if code:
|
||||
_CACHE.pop(code, None)
|
||||
else:
|
||||
_CACHE.clear()
|
||||
|
||||
@staticmethod
|
||||
def default_connection_info() -> ConnectionInfo:
|
||||
db_info = parse_database_url(settings.DATABASE_URL or "")
|
||||
if not db_info:
|
||||
raise ValueError("Invalid DATABASE_URL configuration")
|
||||
return ConnectionInfo(
|
||||
code="default",
|
||||
db_type=db_info["db_type"],
|
||||
host=db_info["host"],
|
||||
port=db_info["port"],
|
||||
user=db_info["user"],
|
||||
password=db_info["password"],
|
||||
database=db_info["database"],
|
||||
is_system=True,
|
||||
display_name=db_info["database"] or "default",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def resolve(
|
||||
cls,
|
||||
code: str,
|
||||
db: Optional[AsyncSession] = None,
|
||||
) -> ConnectionInfo:
|
||||
if code == "default":
|
||||
return cls.default_connection_info()
|
||||
|
||||
now = time.time()
|
||||
cached = _CACHE.get(code)
|
||||
if cached and now - cached[1] < _CACHE_TTL:
|
||||
return cached[0]
|
||||
|
||||
if db is None:
|
||||
async with AsyncSessionLocal() as session:
|
||||
info = await cls._resolve_from_db(session, code)
|
||||
else:
|
||||
info = await cls._resolve_from_db(db, code)
|
||||
|
||||
_CACHE[code] = (info, now)
|
||||
return info
|
||||
|
||||
@classmethod
|
||||
async def _resolve_from_db(cls, db: AsyncSession, code: str) -> ConnectionInfo:
|
||||
result = await db.execute(
|
||||
select(DatabaseConnection).where(
|
||||
DatabaseConnection.code == code,
|
||||
DatabaseConnection.is_deleted == False,
|
||||
)
|
||||
)
|
||||
row = result.scalar_one_or_none()
|
||||
if not row or not row.status:
|
||||
raise ValueError(f"Database connection '{code}' not found or disabled")
|
||||
if row.is_system:
|
||||
return cls.default_connection_info()
|
||||
|
||||
password = decrypt_secret(row.password_enc or "")
|
||||
if row.password_enc and not password:
|
||||
logger.error(
|
||||
"Failed to decrypt password for connection code=%s (check DB_CONN_SECRET_KEY / JWT_SECRET_KEY)",
|
||||
row.code,
|
||||
)
|
||||
|
||||
return ConnectionInfo(
|
||||
code=row.code,
|
||||
db_type=row.db_type,
|
||||
host=row.host,
|
||||
port=row.port,
|
||||
user=row.user or "",
|
||||
password=password,
|
||||
database=row.default_database or "",
|
||||
is_system=False,
|
||||
display_name=row.name,
|
||||
extra_options=row.extra_options or {},
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接 Schema"""
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class DatabaseConnectionBase(BaseModel):
|
||||
name: str = Field(..., max_length=100, description="连接名称")
|
||||
code: str = Field(..., max_length=50, description="连接编码")
|
||||
db_type: str = Field(..., description="postgresql / mysql / sqlserver / oracle")
|
||||
host: str = Field(..., max_length=255)
|
||||
port: int = Field(default=5432, ge=1, le=65535)
|
||||
user: str = Field(default="", max_length=100)
|
||||
default_database: str = Field(default="", max_length=100, alias="defaultDatabase")
|
||||
description: str = Field(default="")
|
||||
status: bool = Field(default=True)
|
||||
application_id: Optional[str] = Field(default=None, alias="applicationId")
|
||||
extra_options: Optional[Dict[str, Any]] = Field(default_factory=dict, alias="extraOptions")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class DatabaseConnectionCreate(DatabaseConnectionBase):
|
||||
password: str = Field(default="", description="密码(明文,仅创建时传入)")
|
||||
|
||||
|
||||
class DatabaseConnectionUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
db_type: Optional[str] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = Field(default=None, ge=1, le=65535)
|
||||
user: Optional[str] = None
|
||||
password: Optional[str] = Field(default=None, description="空则不修改")
|
||||
default_database: Optional[str] = Field(default=None, alias="defaultDatabase")
|
||||
description: Optional[str] = None
|
||||
status: Optional[bool] = None
|
||||
extra_options: Optional[Dict[str, Any]] = Field(default=None, alias="extraOptions")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class DatabaseConnectionResponse(BaseModel):
|
||||
id: str
|
||||
code: str
|
||||
name: str
|
||||
db_type: str
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
default_database: str
|
||||
description: str
|
||||
status: bool
|
||||
is_system: bool
|
||||
has_password: bool
|
||||
application_id: Optional[str] = None
|
||||
application_name: str = ""
|
||||
extra_options: Dict[str, Any] = Field(default_factory=dict)
|
||||
sort: int = 0
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class DatabaseConnectionSimpleOut(BaseModel):
|
||||
code: str
|
||||
name: str
|
||||
db_type: str
|
||||
host: str
|
||||
port: int
|
||||
database: str
|
||||
is_system: bool = False
|
||||
|
||||
|
||||
class DatabaseConnectionTestRequest(BaseModel):
|
||||
db_type: str
|
||||
host: str
|
||||
port: int = 5432
|
||||
user: str = ""
|
||||
password: str = ""
|
||||
default_database: str = Field(default="", alias="defaultDatabase")
|
||||
|
||||
model_config = {"populate_by_name": True}
|
||||
|
||||
|
||||
class DatabaseConnectionTestResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
db_name: Optional[str] = None
|
||||
db_type: Optional[str] = None
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接服务"""
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.timezone import format_datetime
|
||||
from core.database_connection.model import DatabaseConnection
|
||||
from core.database_connection.resolver import ConnectionResolver
|
||||
from core.database_connection.types import ConnectionInfo
|
||||
from core.database_manager.service import AsyncDatabaseManagerService, parse_database_url
|
||||
from utils.secret_crypto import encrypt_secret
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RESOURCE_TYPE = "database_connection"
|
||||
RESOURCE_DISPLAY_NAME = "数据库连接管理"
|
||||
|
||||
_CODE_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_-]{0,49}$")
|
||||
_ALLOWED_DB_TYPES = {"postgresql", "mysql", "sqlserver", "oracle"}
|
||||
_DEFAULT_PORTS = {
|
||||
"postgresql": 5432,
|
||||
"mysql": 3306,
|
||||
"sqlserver": 1433,
|
||||
"oracle": 1521,
|
||||
}
|
||||
|
||||
|
||||
def _default_port(db_type: str) -> int:
|
||||
return _DEFAULT_PORTS.get(db_type, 5432)
|
||||
|
||||
|
||||
class DatabaseConnectionService:
|
||||
@staticmethod
|
||||
def validate_code(code: str) -> None:
|
||||
if code == "default":
|
||||
raise ValueError("code 'default' is reserved for system connection")
|
||||
if not _CODE_PATTERN.match(code):
|
||||
raise ValueError("Invalid connection code format")
|
||||
|
||||
@staticmethod
|
||||
def validate_db_type(db_type: str) -> None:
|
||||
if db_type not in _ALLOWED_DB_TYPES:
|
||||
raise ValueError(f"Unsupported db_type: {db_type}")
|
||||
|
||||
@classmethod
|
||||
async def check_code_exists(cls, db: AsyncSession, code: str, exclude_id: str = None) -> bool:
|
||||
stmt = select(DatabaseConnection.id).where(
|
||||
DatabaseConnection.code == code,
|
||||
DatabaseConnection.is_deleted == False,
|
||||
)
|
||||
if exclude_id:
|
||||
stmt = stmt.where(DatabaseConnection.id != exclude_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@classmethod
|
||||
async def get_list(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
application_id: str = None,
|
||||
name: str = None,
|
||||
code: str = None,
|
||||
status: bool = None,
|
||||
) -> Tuple[List[DatabaseConnection], int]:
|
||||
stmt = select(DatabaseConnection).where(DatabaseConnection.is_deleted == False)
|
||||
if application_id:
|
||||
stmt = stmt.where(DatabaseConnection.application_id == application_id)
|
||||
else:
|
||||
stmt = stmt.where(DatabaseConnection.application_id.is_(None))
|
||||
if name:
|
||||
stmt = stmt.where(DatabaseConnection.name.contains(name))
|
||||
if code:
|
||||
stmt = stmt.where(DatabaseConnection.code.contains(code))
|
||||
if status is not None:
|
||||
stmt = stmt.where(DatabaseConnection.status == status)
|
||||
|
||||
count_stmt = select(func.count()).select_from(stmt.subquery())
|
||||
total = (await db.execute(count_stmt)).scalar() or 0
|
||||
|
||||
stmt = stmt.order_by(
|
||||
DatabaseConnection.sort.desc(),
|
||||
DatabaseConnection.sys_create_datetime.desc(),
|
||||
)
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
items = list((await db.execute(stmt)).scalars().all())
|
||||
return items, total
|
||||
|
||||
@classmethod
|
||||
async def get_all_enabled(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
application_id: str = None,
|
||||
) -> List[DatabaseConnection]:
|
||||
stmt = select(DatabaseConnection).where(
|
||||
DatabaseConnection.is_deleted == False,
|
||||
DatabaseConnection.status == True,
|
||||
DatabaseConnection.is_system == False,
|
||||
)
|
||||
if application_id:
|
||||
stmt = stmt.where(DatabaseConnection.application_id == application_id)
|
||||
else:
|
||||
stmt = stmt.where(DatabaseConnection.application_id.is_(None))
|
||||
stmt = stmt.order_by(DatabaseConnection.sort.desc(), DatabaseConnection.name.asc())
|
||||
return list((await db.execute(stmt)).scalars().all())
|
||||
|
||||
@classmethod
|
||||
async def get_by_id(cls, db: AsyncSession, conn_id: str) -> Optional[DatabaseConnection]:
|
||||
result = await db.execute(
|
||||
select(DatabaseConnection).where(
|
||||
DatabaseConnection.id == conn_id,
|
||||
DatabaseConnection.is_deleted == False,
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def get_by_code(cls, db: AsyncSession, code: str) -> Optional[DatabaseConnection]:
|
||||
result = await db.execute(
|
||||
select(DatabaseConnection).where(
|
||||
DatabaseConnection.code == code,
|
||||
DatabaseConnection.is_deleted == False,
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@classmethod
|
||||
async def create(cls, db: AsyncSession, data: Dict[str, Any]) -> DatabaseConnection:
|
||||
cls.validate_code(data["code"])
|
||||
cls.validate_db_type(data["db_type"])
|
||||
row = DatabaseConnection(
|
||||
application_id=data.get("application_id"),
|
||||
code=data["code"],
|
||||
name=data["name"],
|
||||
db_type=data["db_type"],
|
||||
host=data["host"],
|
||||
port=data.get("port") or _default_port(data["db_type"]),
|
||||
user=data.get("user") or "",
|
||||
password_enc=encrypt_secret((data.get("password") or "").strip()),
|
||||
default_database=data.get("default_database") or "",
|
||||
description=data.get("description") or "",
|
||||
status=data.get("status", True),
|
||||
is_system=False,
|
||||
extra_options=data.get("extra_options") or {},
|
||||
)
|
||||
db.add(row)
|
||||
await db.flush()
|
||||
await db.refresh(row)
|
||||
return row
|
||||
|
||||
@classmethod
|
||||
async def update(cls, db: AsyncSession, row: DatabaseConnection, data: Dict[str, Any]) -> DatabaseConnection:
|
||||
if row.is_system:
|
||||
raise ValueError("System connection cannot be modified")
|
||||
|
||||
if data.get("db_type"):
|
||||
cls.validate_db_type(data["db_type"])
|
||||
for field in ("name", "db_type", "host", "port", "user", "default_database", "description", "status"):
|
||||
if field in data and data[field] is not None:
|
||||
setattr(row, field, data[field])
|
||||
if data.get("extra_options") is not None:
|
||||
row.extra_options = data["extra_options"]
|
||||
if data.get("password"):
|
||||
row.password_enc = encrypt_secret(str(data["password"]).strip())
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(row)
|
||||
ConnectionResolver.invalidate_cache(row.code)
|
||||
return row
|
||||
|
||||
@classmethod
|
||||
async def delete(cls, db: AsyncSession, row: DatabaseConnection) -> None:
|
||||
if row.is_system or row.code == "default":
|
||||
raise ValueError("System connection cannot be deleted")
|
||||
row.is_deleted = True
|
||||
await db.flush()
|
||||
ConnectionResolver.invalidate_cache(row.code)
|
||||
|
||||
@classmethod
|
||||
async def test_connection_info(cls, info: ConnectionInfo) -> Dict[str, Any]:
|
||||
service = AsyncDatabaseManagerService.from_connection_info(info)
|
||||
result = await service.test_connection()
|
||||
if not result.get("success"):
|
||||
logger.error(
|
||||
"Database connection registry test failed: code=%s message=%s",
|
||||
info.code,
|
||||
result.get("message"),
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def test_config(cls, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
db_type = config["db_type"]
|
||||
cls.validate_db_type(db_type)
|
||||
info = ConnectionInfo(
|
||||
code="__test__",
|
||||
db_type=db_type,
|
||||
host=config["host"],
|
||||
port=config.get("port") or _default_port(db_type),
|
||||
user=config.get("user") or "",
|
||||
password=config.get("password") or "",
|
||||
database=config.get("default_database") or "",
|
||||
)
|
||||
return await cls.test_connection_info(info)
|
||||
|
||||
@classmethod
|
||||
async def test_saved(cls, db: AsyncSession, row: DatabaseConnection) -> Dict[str, Any]:
|
||||
info = await ConnectionResolver.resolve(row.code, db)
|
||||
return await cls.test_connection_info(info)
|
||||
|
||||
@classmethod
|
||||
def to_response_dict(cls, row: DatabaseConnection) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": row.id,
|
||||
"code": row.code,
|
||||
"name": row.name,
|
||||
"db_type": row.db_type,
|
||||
"host": row.host,
|
||||
"port": row.port,
|
||||
"user": row.user or "",
|
||||
"default_database": row.default_database or "",
|
||||
"description": row.description or "",
|
||||
"status": row.status,
|
||||
"is_system": row.is_system,
|
||||
"has_password": bool(row.password_enc),
|
||||
"application_id": row.application_id,
|
||||
"extra_options": row.extra_options or {},
|
||||
"sort": row.sort or 0,
|
||||
"sys_create_datetime": format_datetime(row.sys_create_datetime) if row.sys_create_datetime else None,
|
||||
"sys_update_datetime": format_datetime(row.sys_update_datetime) if row.sys_update_datetime else None,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def get_manager_configs(
|
||||
cls,
|
||||
db: AsyncSession,
|
||||
application_id: str = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
configs: List[Dict[str, Any]] = []
|
||||
default_info = ConnectionResolver.default_connection_info()
|
||||
configs.append({
|
||||
"db_name": "default",
|
||||
"name": default_info.display_name or default_info.database or "default",
|
||||
"display_name": default_info.display_name or default_info.database or "default",
|
||||
"db_type": default_info.db_type,
|
||||
"host": default_info.host,
|
||||
"port": default_info.port,
|
||||
"database": default_info.database,
|
||||
"user": default_info.user,
|
||||
"has_password": bool(default_info.password),
|
||||
"is_system": True,
|
||||
})
|
||||
|
||||
custom_rows = await cls.get_all_enabled(db, application_id)
|
||||
for row in custom_rows:
|
||||
configs.append({
|
||||
"db_name": row.code,
|
||||
"name": row.name,
|
||||
"display_name": row.name,
|
||||
"db_type": row.db_type,
|
||||
"host": row.host,
|
||||
"port": row.port,
|
||||
"database": row.default_database or "",
|
||||
"user": row.user or "",
|
||||
"has_password": bool(row.password_enc),
|
||||
"is_system": False,
|
||||
})
|
||||
return configs
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接信息类型"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionInfo:
|
||||
code: str
|
||||
db_type: str
|
||||
host: str
|
||||
port: int
|
||||
user: str
|
||||
password: str
|
||||
database: str
|
||||
is_system: bool = False
|
||||
display_name: str = ""
|
||||
extra_options: Optional[Dict[str, Any]] = None
|
||||
|
||||
def to_handler_kwargs(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"user": self.user,
|
||||
"password": self.password,
|
||||
"database": self.database,
|
||||
}
|
||||
Reference in New Issue
Block a user