元数据库目录结构变更

This commit is contained in:
2026-03-13 16:15:09 +08:00
parent 5041922c84
commit c137adedad
14 changed files with 128 additions and 8 deletions
@@ -0,0 +1,203 @@
from dataclasses import dataclass
from typing import Optional, List
from uuid import UUID
from cryptography.fernet import InvalidToken
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.encryption import (
get_database_encryptor,
get_encryptor,
is_database_encryption_configured,
is_encryption_configured,
)
from app.infra.db.metadb import models
def _normalize_postgres_dsn(dsn: str) -> str:
if not dsn or "://" not in dsn:
return dsn
scheme, rest = dsn.split("://", 1)
if scheme not in ("postgresql", "postgres", "postgresql+psycopg"):
return dsn
if "@" not in rest:
return dsn
userinfo, hostinfo = rest.rsplit("@", 1)
if ":" not in userinfo:
return dsn
username, password = userinfo.split(":", 1)
if "@" not in password:
return dsn
password = password.replace("@", "%40")
return f"{scheme}://{username}:{password}@{hostinfo}"
@dataclass(frozen=True)
class ProjectDbRouting:
project_id: UUID
db_role: str
db_type: str
dsn: str
pool_min_size: int
pool_max_size: int
@dataclass(frozen=True)
class ProjectGeoServerInfo:
project_id: UUID
gs_base_url: Optional[str]
gs_admin_user: Optional[str]
gs_admin_password: Optional[str]
gs_datastore_name: str
default_extent: Optional[dict]
srid: int
@dataclass(frozen=True)
class ProjectSummary:
project_id: UUID
name: str
code: str
description: Optional[str]
gs_workspace: str
status: str
project_role: str
class MetadataRepository:
"""元数据访问层(system_hub"""
def __init__(self, session: AsyncSession):
self.session = session
async def get_user_by_keycloak_id(self, keycloak_id: UUID) -> Optional[models.User]:
result = await self.session.execute(
select(models.User).where(models.User.keycloak_id == keycloak_id)
)
return result.scalar_one_or_none()
async def get_user_by_username(self, username: str) -> Optional[models.User]:
result = await self.session.execute(
select(models.User).where(models.User.username == username)
)
return result.scalar_one_or_none()
async def get_project_by_id(self, project_id: UUID) -> Optional[models.Project]:
result = await self.session.execute(
select(models.Project).where(models.Project.id == project_id)
)
return result.scalar_one_or_none()
async def get_membership_role(
self, project_id: UUID, user_id: UUID
) -> Optional[str]:
result = await self.session.execute(
select(models.UserProjectMembership.project_role).where(
models.UserProjectMembership.project_id == project_id,
models.UserProjectMembership.user_id == user_id,
)
)
return result.scalar_one_or_none()
async def get_project_db_routing(
self, project_id: UUID, db_role: str
) -> Optional[ProjectDbRouting]:
result = await self.session.execute(
select(models.ProjectDatabase).where(
models.ProjectDatabase.project_id == project_id,
models.ProjectDatabase.db_role == db_role,
)
)
record = result.scalar_one_or_none()
if not record:
return None
if not is_database_encryption_configured():
raise ValueError("DATABASE_ENCRYPTION_KEY is not configured")
encryptor = get_database_encryptor()
try:
dsn = encryptor.decrypt(record.dsn_encrypted)
except InvalidToken:
raise ValueError(
"Failed to decrypt project DB DSN: DATABASE_ENCRYPTION_KEY mismatch "
"or invalid dsn_encrypted value"
)
dsn = _normalize_postgres_dsn(dsn)
return ProjectDbRouting(
project_id=record.project_id,
db_role=record.db_role,
db_type=record.db_type,
dsn=dsn,
pool_min_size=record.pool_min_size,
pool_max_size=record.pool_max_size,
)
async def get_geoserver_config(
self, project_id: UUID
) -> Optional[ProjectGeoServerInfo]:
result = await self.session.execute(
select(models.ProjectGeoServerConfig).where(
models.ProjectGeoServerConfig.project_id == project_id
)
)
record = result.scalar_one_or_none()
if not record:
return None
if record.gs_admin_password_encrypted:
if is_encryption_configured():
encryptor = get_encryptor()
password = encryptor.decrypt(record.gs_admin_password_encrypted)
else:
password = record.gs_admin_password_encrypted
else:
password = None
return ProjectGeoServerInfo(
project_id=record.project_id,
gs_base_url=record.gs_base_url,
gs_admin_user=record.gs_admin_user,
gs_admin_password=password,
gs_datastore_name=record.gs_datastore_name,
default_extent=record.default_extent,
srid=record.srid,
)
async def list_projects_for_user(self, user_id: UUID) -> List[ProjectSummary]:
stmt = (
select(models.Project, models.UserProjectMembership.project_role)
.join(
models.UserProjectMembership,
models.UserProjectMembership.project_id == models.Project.id,
)
.where(models.UserProjectMembership.user_id == user_id)
.order_by(models.Project.name)
)
result = await self.session.execute(stmt)
return [
ProjectSummary(
project_id=project.id,
name=project.name,
code=project.code,
description=project.description,
gs_workspace=project.gs_workspace,
status=project.status,
project_role=role,
)
for project, role in result.all()
]
async def list_all_projects(self) -> List[ProjectSummary]:
result = await self.session.execute(
select(models.Project).order_by(models.Project.name)
)
return [
ProjectSummary(
project_id=project.id,
name=project.name,
code=project.code,
description=project.description,
gs_workspace=project.gs_workspace,
status=project.status,
project_role="owner",
)
for project in result.scalars().all()
]