refactor: normalize script_builder_messages into separate table

Extract JSONB messages array from script_builder_sessions into a proper
script_builder_messages table with individual columns for role, content,
script, tokens, etc. Migration handles data migration from JSONB to rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-21 21:06:58 -04:00
parent 0b261ee21a
commit b801f6cac5
9 changed files with 290 additions and 61 deletions

View File

@@ -9,10 +9,12 @@ from app.core.database import get_db
from app.core.rate_limit import limiter
from app.api.deps import get_current_active_user
from app.models.user import User
from app.models.script_builder_session import ScriptBuilderSession
from app.schemas.script_builder import (
ScriptBuilderCreateRequest,
ScriptBuilderMessageRequest,
ScriptBuilderMessageResponse,
ScriptBuilderMessageSchema,
ScriptBuilderSessionDetail,
ScriptBuilderSessionSummary,
SaveToLibraryRequest,
@@ -25,6 +27,39 @@ router = APIRouter(prefix="/scripts/builder", tags=["script-builder"])
MAX_SESSIONS_PER_USER = 5
def _session_to_detail(session: ScriptBuilderSession) -> ScriptBuilderSessionDetail:
"""Convert a session ORM object (with message_records loaded) to detail schema."""
messages = [
ScriptBuilderMessageSchema.model_validate(m)
for m in session.message_records
]
return ScriptBuilderSessionDetail(
id=session.id,
language=session.language,
title=session.title,
messages=messages,
latest_script=session.latest_script,
latest_script_filename=session.latest_script_filename,
message_count=len([m for m in messages if m.role == "user"]),
ai_session_id=session.ai_session_id,
created_at=session.created_at,
updated_at=session.updated_at,
)
def _session_to_summary(session: ScriptBuilderSession) -> ScriptBuilderSessionSummary:
"""Convert a session ORM object to summary schema (no messages needed)."""
return ScriptBuilderSessionSummary(
id=session.id,
language=session.language,
title=session.title,
message_count=0, # Summary doesn't eagerly load messages
latest_script_filename=session.latest_script_filename,
created_at=session.created_at,
updated_at=session.updated_at,
)
@router.post("/sessions", response_model=ScriptBuilderSessionDetail, status_code=201)
async def create_session(
data: ScriptBuilderCreateRequest,
@@ -47,7 +82,9 @@ async def create_session(
language=data.language,
)
await db.commit()
return ScriptBuilderSessionDetail.model_validate(session)
# Re-fetch with message_records loaded
session = await script_builder_service.get_session(db, session.id, current_user.id)
return _session_to_detail(session)
@router.get("/sessions", response_model=list[ScriptBuilderSessionSummary])
@@ -61,7 +98,7 @@ async def list_sessions(
sessions = await script_builder_service.list_sessions(
db=db, user_id=current_user.id, limit=limit, offset=offset
)
return [ScriptBuilderSessionSummary.model_validate(s) for s in sessions]
return [_session_to_summary(s) for s in sessions]
@router.get("/sessions/{session_id}", response_model=ScriptBuilderSessionDetail)
@@ -74,7 +111,7 @@ async def get_session(
session = await script_builder_service.get_session(db, session_id, current_user.id)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
return ScriptBuilderSessionDetail.model_validate(session)
return _session_to_detail(session)
@router.post(