136 lines
5.0 KiB
Python
136 lines
5.0 KiB
Python
"""SQLAlchemy-backed light-app registry store (global / shared)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from deerflow.persistence.light_apps.base import DuplicateLightAppNameError, LightAppStore
|
|
from deerflow.persistence.light_apps.model import LightAppRow
|
|
|
|
# Columns a caller may set/patch (everything except identity + timestamps).
|
|
_WRITABLE = {
|
|
"name",
|
|
"description",
|
|
"app_type",
|
|
"url",
|
|
"route_params",
|
|
"mount_location",
|
|
"icon",
|
|
"enabled",
|
|
"sort_order",
|
|
"updated_by",
|
|
}
|
|
|
|
|
|
def _to_dict(row: LightAppRow) -> dict[str, Any]:
|
|
data = row.to_dict()
|
|
for key in ("created_at", "updated_at"):
|
|
value = data.get(key)
|
|
if isinstance(value, datetime):
|
|
data[key] = value.isoformat()
|
|
data["route_params"] = data.pop("route_params_json", None) or []
|
|
return data
|
|
|
|
|
|
def _column_for(key: str) -> str:
|
|
return "route_params_json" if key == "route_params" else key
|
|
|
|
|
|
class LightAppRepository(LightAppStore):
|
|
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
|
|
self._sf = session_factory
|
|
|
|
async def list_apps(
|
|
self,
|
|
*,
|
|
app_type: str | None = None,
|
|
mount_location: str | None = None,
|
|
enabled_only: bool = False,
|
|
) -> list[dict[str, Any]]:
|
|
stmt = select(LightAppRow)
|
|
if app_type:
|
|
stmt = stmt.where(LightAppRow.app_type == app_type)
|
|
if mount_location:
|
|
stmt = stmt.where(LightAppRow.mount_location == mount_location)
|
|
if enabled_only:
|
|
stmt = stmt.where(LightAppRow.enabled.is_(True))
|
|
stmt = stmt.order_by(LightAppRow.sort_order.asc(), LightAppRow.created_at.desc())
|
|
async with self._sf() as session:
|
|
result = await session.execute(stmt)
|
|
return [_to_dict(row) for row in result.scalars()]
|
|
|
|
async def get_app(self, app_id: str) -> dict[str, Any] | None:
|
|
async with self._sf() as session:
|
|
row = await session.get(LightAppRow, app_id)
|
|
return _to_dict(row) if row is not None else None
|
|
|
|
async def _name_taken(self, session: AsyncSession, name: str, *, exclude_id: str | None = None) -> bool:
|
|
stmt = select(LightAppRow.app_id).where(LightAppRow.name == name)
|
|
if exclude_id is not None:
|
|
stmt = stmt.where(LightAppRow.app_id != exclude_id)
|
|
result = await session.execute(stmt.limit(1))
|
|
return result.first() is not None
|
|
|
|
async def create_app(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
row = LightAppRow(
|
|
app_id=data.get("app_id") or uuid.uuid4().hex[:12],
|
|
name=data["name"],
|
|
description=data.get("description"),
|
|
app_type=data["app_type"],
|
|
url=data["url"],
|
|
route_params_json=data.get("route_params") or [],
|
|
mount_location=data.get("mount_location"),
|
|
icon=data.get("icon"),
|
|
enabled=data.get("enabled", True),
|
|
sort_order=data.get("sort_order", 0),
|
|
created_by=data.get("created_by"),
|
|
updated_by=data.get("created_by"),
|
|
)
|
|
async with self._sf() as session:
|
|
if await self._name_taken(session, row.name):
|
|
raise DuplicateLightAppNameError(row.name)
|
|
session.add(row)
|
|
try:
|
|
await session.commit()
|
|
except IntegrityError as exc:
|
|
await session.rollback()
|
|
raise DuplicateLightAppNameError(row.name) from exc
|
|
await session.refresh(row)
|
|
return _to_dict(row)
|
|
|
|
async def update_app(self, app_id: str, data: dict[str, Any]) -> dict[str, Any] | None:
|
|
async with self._sf() as session:
|
|
row = await session.get(LightAppRow, app_id)
|
|
if row is None:
|
|
return None
|
|
new_name = data.get("name")
|
|
if new_name is not None and new_name != row.name and await self._name_taken(session, new_name, exclude_id=app_id):
|
|
raise DuplicateLightAppNameError(new_name)
|
|
for key, value in data.items():
|
|
if key not in _WRITABLE:
|
|
continue
|
|
setattr(row, _column_for(key), value)
|
|
row.updated_at = datetime.now(UTC)
|
|
try:
|
|
await session.commit()
|
|
except IntegrityError as exc:
|
|
await session.rollback()
|
|
raise DuplicateLightAppNameError(new_name or row.name) from exc
|
|
await session.refresh(row)
|
|
return _to_dict(row)
|
|
|
|
async def delete_app(self, app_id: str) -> bool:
|
|
async with self._sf() as session:
|
|
row = await session.get(LightAppRow, app_id)
|
|
if row is None:
|
|
return False
|
|
await session.delete(row)
|
|
await session.commit()
|
|
return True
|