from datetime import datetime, timezone from sqlalchemy import select, text from fabledassistant.models import async_session from fabledassistant.models.note_version import NoteVersion # Maximum snapshots retained per note. MAX_VERSIONS = 50 # Minimum seconds between snapshots. Prevents autosave from consuming all # slots — with autosave at 60 s intervals, a 5-minute gate means at most # one snapshot per editing session segment rather than one per save tick. MIN_VERSION_INTERVAL_SECONDS = 300 async def create_version( user_id: int, note_id: int, body: str, title: str, tags: list[str] | None = None ) -> NoteVersion | None: async with async_session() as session: # Fetch the most recent snapshot once for both checks. recent = await session.execute( select(NoteVersion) .where(NoteVersion.note_id == note_id, NoteVersion.user_id == user_id) .order_by(NoteVersion.created_at.desc()) .limit(1) ) last = recent.scalars().first() if last is not None: # Skip if content is identical — no change, no snapshot (git semantics). if ( last.body == body and last.title == title and sorted(last.tags or []) == sorted(tags or []) ): return None # Skip if within the minimum interval to prevent rapid-fire autosave snapshots. last_at = last.created_at if last_at.tzinfo is None: last_at = last_at.replace(tzinfo=timezone.utc) age = (datetime.now(timezone.utc) - last_at).total_seconds() if age < MIN_VERSION_INTERVAL_SECONDS: return None version = NoteVersion(note_id=note_id, user_id=user_id, body=body, title=title, tags=tags or []) session.add(version) await session.commit() await session.refresh(version) # Prune versions beyond MAX_VERSIONS await session.execute( text(""" DELETE FROM note_versions WHERE id IN ( SELECT id FROM note_versions WHERE note_id = :note_id AND user_id = :user_id ORDER BY created_at DESC OFFSET :max_versions ) """).bindparams(note_id=note_id, user_id=user_id, max_versions=MAX_VERSIONS) ) await session.commit() return version async def list_versions(user_id: int, note_id: int) -> list[NoteVersion]: async with async_session() as session: result = await session.execute( select(NoteVersion) .where(NoteVersion.note_id == note_id, NoteVersion.user_id == user_id) .order_by(NoteVersion.created_at.desc()) ) return list(result.scalars().all()) async def get_version(user_id: int, note_id: int, version_id: int) -> NoteVersion | None: async with async_session() as session: result = await session.execute( select(NoteVersion).where( NoteVersion.id == version_id, NoteVersion.note_id == note_id, NoteVersion.user_id == user_id, ) ) return result.scalars().first()