Files

147 lines
5.1 KiB
Python

"""Sync service for VoIdea.
Handles pull/push sync of user data across devices.
Change tracking via updated_at timestamps.
"""
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.idea import Idea
from app.models.user import User
class SyncService:
def __init__(self, db: AsyncSession):
self.db = db
async def pull(self, user: User, last_sync: datetime | None = None) -> dict:
"""Pull changes since last_sync.
Args:
user: Authenticated user
last_sync: Last sync timestamp (None = full sync)
Returns:
Dict with changes list and sync_token
"""
query = select(Idea).where(Idea.user_id == user.id)
if last_sync:
query = query.where(Idea.updated_at > last_sync)
result = await self.db.execute(query)
ideas = result.scalars().all()
changes = [
{
"entity": "idea",
"action": "update",
"id": str(idea.id),
"data": {
"title": idea.title,
"content": idea.content,
"status": idea.status,
"updated_at": idea.updated_at.isoformat() if idea.updated_at else None,
},
}
for idea in ideas
]
return {
"status": "ok",
"changes": changes,
"sync_token": datetime.now(timezone.utc).isoformat(),
}
async def push(self, user: User, device_id: str, changes: list[dict[str, Any]]) -> dict:
"""Push changes from device.
Args:
user: Authenticated user
device_id: Source device identifier
changes: List of change dicts
Returns:
Dict with applied changes and conflicts
"""
applied = []
conflicts = []
for change in changes:
entity = change.get("entity", "idea")
action = change.get("action", "update")
change_id = change.get("id")
if entity != "idea":
applied.append({"id": change_id, "status": "skipped", "reason": "unsupported_entity"})
continue
data = change.get("data", {})
if action == "delete" and change_id:
query = select(Idea).where(Idea.id == UUID(change_id), Idea.user_id == user.id)
result = await self.db.execute(query)
idea = result.scalar_one_or_none()
if idea:
await self.db.delete(idea)
applied.append({"id": change_id, "status": "deleted"})
else:
applied.append({"id": change_id, "status": "not_found"})
continue
if change_id:
query = select(Idea).where(Idea.id == UUID(change_id), Idea.user_id == user.id)
result = await self.db.execute(query)
existing = result.scalar_one_or_none()
if existing:
incoming_ts = data.get("updated_at")
if incoming_ts and existing.updated_at:
incoming_dt = datetime.fromisoformat(incoming_ts)
if existing.updated_at.replace(tzinfo=timezone.utc) > incoming_dt.replace(tzinfo=timezone.utc):
conflicts.append({
"id": change_id,
"server_version": existing.updated_at.isoformat(),
"client_version": incoming_ts,
"message": "Server has newer version",
})
continue
for key, value in data.items():
if key != "updated_at" and hasattr(existing, key):
setattr(existing, key, value)
applied.append({"id": change_id, "status": "updated"})
else:
new_idea = Idea(
id=UUID(change_id) if change_id else uuid4(),
user_id=user.id,
title=data.get("title", ""),
content=data.get("content", ""),
status=data.get("status", "draft"),
)
self.db.add(new_idea)
applied.append({"id": change_id or str(new_idea.id), "status": "created"})
else:
new_idea = Idea(
user_id=user.id,
title=data.get("title", ""),
content=data.get("content", ""),
status=data.get("status", "draft"),
)
self.db.add(new_idea)
applied.append({"id": str(new_idea.id), "status": "created"})
await self.db.commit()
return {
"status": "ok",
"changes": applied,
"conflicts": conflicts,
"sync_token": datetime.now(timezone.utc).isoformat(),
}