feat(core): implement ConnectionService and DbExecutor for direct DB insert (US11-US12)
- ConnectionService: CRUD for DatabaseConnection in GlobalSettings with EncryptionManager-backed password encryption/decryption, test connectivity - DbExecutor: asyncpg (PostgreSQL) and clickhouse-connect (ClickHouse) drivers with per-connection connection pooling - config_models.py: promote GlobalSettings.connections from list[dict] stub to list[DatabaseConnection] with full Pydantic model validation - settings.py: +5 CRUD endpoints for connections (create, read, update, delete, test) - requirements.txt: add asyncpg>=0.29.0, clickhouse-connect>=0.7.0 - seed_permissions.py: register settings.connections.manage permission
This commit is contained in:
@@ -15,6 +15,9 @@
|
||||
# @BRIEF Contract for database/environment connection configuration models.
|
||||
# #endregion ConnectionContracts
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from ..models.storage import StorageConfig
|
||||
@@ -173,6 +176,53 @@ class FeaturesConfig(BaseModel):
|
||||
# #endregion FeaturesConfig
|
||||
|
||||
|
||||
# #region DatabaseConnection [C:1] [TYPE DataClass] [SEMANTICS settings,connections,database]
|
||||
# @ingroup Core
|
||||
# @BRIEF A reusable database connection configuration stored in GlobalSettings.connections.
|
||||
# @RATIONALE Typed model prevents config corruption from ad-hoc dicts. Replaces the existing
|
||||
# `connections: list[dict]` stub. connection_id references the UUID in this list — not a DB FK.
|
||||
# @REJECTED Keeping `list[dict]` was rejected — would require per-field validation in every consumer.
|
||||
# A separate DB table was rejected — connections are configuration, not domain data.
|
||||
class DatabaseConnection(BaseModel):
|
||||
id: str = Field(default_factory=lambda: uuid.uuid4().hex[:12])
|
||||
name: str
|
||||
host: str
|
||||
port: int = 5432
|
||||
database: str
|
||||
username: str
|
||||
password: str # Fernet-encrypted at rest; masked as "********" in API responses
|
||||
dialect: str = "postgresql"
|
||||
extra_params: dict = Field(default_factory=dict)
|
||||
pool_size: int = Field(default=5, ge=1, le=100)
|
||||
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
updated_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
||||
|
||||
@field_validator("dialect")
|
||||
@classmethod
|
||||
def validate_dialect(cls, v: str) -> str:
|
||||
allowed = {"postgresql", "clickhouse", "mysql"}
|
||||
if v.lower() not in allowed:
|
||||
raise ValueError(f"Unsupported dialect: {v}. Allowed: {', '.join(sorted(allowed))}")
|
||||
return v.lower()
|
||||
|
||||
@field_validator("port")
|
||||
@classmethod
|
||||
def validate_port(cls, v: int) -> int:
|
||||
if v < 1 or v > 65535:
|
||||
raise ValueError(f"Port must be between 1 and 65535, got {v}")
|
||||
return v
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
if not v or not v.strip():
|
||||
raise ValueError("Connection name must not be empty")
|
||||
return v.strip()
|
||||
|
||||
|
||||
# #endregion DatabaseConnection
|
||||
|
||||
|
||||
# #region GlobalSettings [TYPE DataClass]
|
||||
# @ingroup Core
|
||||
# @BRIEF Represents global application settings.
|
||||
@@ -182,7 +232,7 @@ class GlobalSettings(BaseModel):
|
||||
default_environment_id: str | None = None
|
||||
logging: LoggingConfig = Field(default_factory=LoggingConfig)
|
||||
features: FeaturesConfig = Field(default_factory=FeaturesConfig)
|
||||
connections: list[dict] = []
|
||||
connections: list[DatabaseConnection] = []
|
||||
llm: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"providers": [],
|
||||
|
||||
Reference in New Issue
Block a user