124 lines
4.2 KiB
Python
124 lines
4.2 KiB
Python
"""SQLAlchemy-backed roundtable business-chain store.
|
|
|
|
The ``seats`` JSON column is stored as a serialized string in
|
|
``PortableLongText`` and round-tripped here with ``json.dumps`` / ``json.loads``
|
|
— same approach as ``roundtable_drafts`` / ``ai_writing_sessions``.
|
|
|
|
All write/read paths are scoped by ``user_id`` for ownership isolation; a row
|
|
that belongs to another user is treated as not found.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from deerflow.persistence.roundtable_chains.model import RoundtableChainRow
|
|
|
|
_UNSET = object()
|
|
|
|
|
|
def _loads(value: Any) -> Any:
|
|
if not isinstance(value, str):
|
|
return value
|
|
if not value:
|
|
return None
|
|
try:
|
|
return json.loads(value)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _dumps(value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
class RoundtableChainRepository:
|
|
def __init__(self, session_factory: async_sessionmaker[AsyncSession]) -> None:
|
|
self._sf = session_factory
|
|
|
|
@staticmethod
|
|
def _to_dict(row: RoundtableChainRow) -> dict[str, Any]:
|
|
return {
|
|
"id": row.id,
|
|
"title": row.title,
|
|
"description": row.description,
|
|
"seats": _loads(row.seats) or [],
|
|
"created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at,
|
|
"updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at,
|
|
}
|
|
|
|
async def list_chains(self, user_id: str, *, limit: int = 200) -> list[dict[str, Any]]:
|
|
stmt = (
|
|
select(RoundtableChainRow)
|
|
.where(RoundtableChainRow.user_id == user_id)
|
|
.order_by(RoundtableChainRow.updated_at.desc())
|
|
.limit(limit)
|
|
)
|
|
async with self._sf() as session:
|
|
result = await session.execute(stmt)
|
|
return [self._to_dict(row) for row in result.scalars()]
|
|
|
|
async def get_chain(self, chain_id: str, user_id: str) -> dict[str, Any] | None:
|
|
async with self._sf() as session:
|
|
row = await session.get(RoundtableChainRow, chain_id)
|
|
if row is None or row.user_id != user_id:
|
|
return None
|
|
return self._to_dict(row)
|
|
|
|
async def create_chain(self, user_id: str, data: dict[str, Any]) -> dict[str, Any]:
|
|
now = datetime.now(UTC)
|
|
row = RoundtableChainRow(
|
|
id=data["id"],
|
|
user_id=user_id,
|
|
title=(data.get("title") or "").strip(),
|
|
description=(data.get("description") or None),
|
|
seats=_dumps(data.get("seats") or []),
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
async with self._sf() as session:
|
|
session.add(row)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return self._to_dict(row)
|
|
|
|
async def update_chain(
|
|
self,
|
|
chain_id: str,
|
|
user_id: str,
|
|
*,
|
|
title=_UNSET,
|
|
description=_UNSET,
|
|
seats=_UNSET,
|
|
) -> dict[str, Any] | None:
|
|
async with self._sf() as session:
|
|
row = await session.get(RoundtableChainRow, chain_id)
|
|
if row is None or row.user_id != user_id:
|
|
return None
|
|
if title is not _UNSET and title is not None:
|
|
row.title = str(title).strip()
|
|
if description is not _UNSET:
|
|
row.description = description or None
|
|
if seats is not _UNSET:
|
|
row.seats = _dumps(seats)
|
|
row.updated_at = datetime.now(UTC)
|
|
await session.commit()
|
|
await session.refresh(row)
|
|
return self._to_dict(row)
|
|
|
|
async def delete_chain(self, chain_id: str, user_id: str) -> bool:
|
|
async with self._sf() as session:
|
|
row = await session.get(RoundtableChainRow, chain_id)
|
|
if row is None or row.user_id != user_id:
|
|
return False
|
|
await session.delete(row)
|
|
await session.commit()
|
|
return True
|