173 lines
6.6 KiB
Python
173 lines
6.6 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import asdict
|
|
from datetime import datetime, timezone
|
|
from uuid import uuid4
|
|
|
|
from app.domain.editor import EditorSession, VariantChange
|
|
from app.domain.editor_builder import build_runtime_payload_from_changes
|
|
from app.repositories.directus.variant_changes import VariantChangeRepository
|
|
from app.repositories.native.editor_sessions import EditorSessionRepository
|
|
from app.schemas.auth import AuthenticatedUser
|
|
from app.schemas.editor import (
|
|
BuildPreviewRequest,
|
|
BuildPreviewResponse,
|
|
EditorChangeListResponse,
|
|
EditorChangeRead,
|
|
EditorSessionCreateRequest,
|
|
EditorSessionRead,
|
|
EditorSessionUpdateRequest,
|
|
SaveVariantChangesRequest,
|
|
)
|
|
|
|
|
|
class EditorService:
|
|
"""Application service for visual-editor workflows."""
|
|
|
|
def __init__(
|
|
self,
|
|
change_repository: VariantChangeRepository | None = None,
|
|
session_repository: EditorSessionRepository | None = None,
|
|
) -> None:
|
|
self.change_repository = change_repository or VariantChangeRepository()
|
|
self.session_repository = session_repository or EditorSessionRepository()
|
|
|
|
async def create_session(
|
|
self,
|
|
request: EditorSessionCreateRequest,
|
|
current_user: AuthenticatedUser,
|
|
) -> EditorSessionRead:
|
|
session = EditorSession(
|
|
variant_id=request.variant_id,
|
|
mode=request.mode,
|
|
base_url=request.base_url,
|
|
actor_id=current_user.id,
|
|
actor_email=current_user.email,
|
|
)
|
|
await self.session_repository.create(session)
|
|
return EditorSessionRead.model_validate(asdict(session))
|
|
|
|
async def get_session(self, session_id: str) -> EditorSessionRead | None:
|
|
session = await self.session_repository.get(session_id)
|
|
if not session:
|
|
return None
|
|
return EditorSessionRead.model_validate(asdict(session))
|
|
|
|
async def update_session(
|
|
self,
|
|
session_id: str,
|
|
request: EditorSessionUpdateRequest,
|
|
) -> EditorSessionRead | None:
|
|
session = await self.session_repository.get(session_id)
|
|
if not session:
|
|
return None
|
|
|
|
if request.status is not None:
|
|
session.status = request.status
|
|
if request.draft_changes is not None:
|
|
session.draft_changes = request.draft_changes
|
|
session.updated_at = datetime.now(timezone.utc)
|
|
|
|
await self.session_repository.update(session)
|
|
return EditorSessionRead.model_validate(asdict(session))
|
|
|
|
async def delete_session(self, session_id: str) -> None:
|
|
await self.session_repository.delete(session_id)
|
|
|
|
async def list_changes(self, variant_id: str) -> EditorChangeListResponse:
|
|
items = await self.change_repository.list(params={"filter[variant_id][_eq]": variant_id})
|
|
mapped = [EditorChangeRead.model_validate(item) for item in items]
|
|
return EditorChangeListResponse(items=mapped)
|
|
|
|
async def list_changes_with_access_token(
|
|
self,
|
|
variant_id: str,
|
|
access_token: str,
|
|
) -> EditorChangeListResponse:
|
|
items = await self.change_repository.list(
|
|
params={"filter[variant_id][_eq]": variant_id},
|
|
access_token=access_token,
|
|
)
|
|
mapped = [EditorChangeRead.model_validate(item) for item in items]
|
|
return EditorChangeListResponse(items=mapped)
|
|
|
|
async def save_changes(
|
|
self,
|
|
variant_id: str,
|
|
request: SaveVariantChangesRequest,
|
|
) -> EditorChangeListResponse:
|
|
# Full-replace semantics: delete any existing changes not present in the request.
|
|
existing = await self.change_repository.list(params={"filter[variant_id][_eq]": variant_id})
|
|
incoming_ids = {item.id for item in request.items if item.id}
|
|
for existing_item in existing:
|
|
if str(existing_item["id"]) not in incoming_ids:
|
|
await self.change_repository.delete(str(existing_item["id"]))
|
|
|
|
saved_items: list[EditorChangeRead] = []
|
|
for item in request.items:
|
|
payload = item.model_dump()
|
|
payload["variant_id"] = variant_id
|
|
|
|
change_id = payload.pop("id", None)
|
|
if change_id:
|
|
saved = await self.change_repository.update(change_id, payload)
|
|
else:
|
|
payload["id"] = str(uuid4())
|
|
saved = await self.change_repository.create(payload)
|
|
|
|
saved_items.append(EditorChangeRead.model_validate(saved))
|
|
|
|
return EditorChangeListResponse(items=saved_items)
|
|
|
|
async def save_changes_with_access_token(
|
|
self,
|
|
variant_id: str,
|
|
request: SaveVariantChangesRequest,
|
|
access_token: str,
|
|
) -> EditorChangeListResponse:
|
|
# Full-replace semantics: delete any existing changes not present in the request.
|
|
existing = await self.change_repository.list(
|
|
params={"filter[variant_id][_eq]": variant_id},
|
|
access_token=access_token,
|
|
)
|
|
incoming_ids = {item.id for item in request.items if item.id}
|
|
for existing_item in existing:
|
|
if str(existing_item["id"]) not in incoming_ids:
|
|
await self.change_repository.delete(str(existing_item["id"]), access_token=access_token)
|
|
|
|
saved_items: list[EditorChangeRead] = []
|
|
for item in request.items:
|
|
payload = item.model_dump()
|
|
payload["variant_id"] = variant_id
|
|
|
|
change_id = payload.pop("id", None)
|
|
if change_id:
|
|
saved = await self.change_repository.update(change_id, payload, access_token=access_token)
|
|
else:
|
|
payload["id"] = str(uuid4())
|
|
saved = await self.change_repository.create(payload, access_token=access_token)
|
|
|
|
saved_items.append(EditorChangeRead.model_validate(saved))
|
|
|
|
return EditorChangeListResponse(items=saved_items)
|
|
|
|
async def build_preview(self, request: BuildPreviewRequest) -> BuildPreviewResponse:
|
|
changes = [
|
|
VariantChange(
|
|
id=item.id or str(uuid4()),
|
|
variant_id=request.variant_id,
|
|
change_type=item.change_type,
|
|
selector_type=item.selector_type,
|
|
selector_value=item.selector_value,
|
|
sort_order=item.sort_order,
|
|
payload=item.payload,
|
|
)
|
|
for item in request.items
|
|
]
|
|
preview = build_runtime_payload_from_changes(request.variant_id, changes)
|
|
return BuildPreviewResponse(
|
|
variant_id=preview.variant_id,
|
|
generated_at=preview.generated_at,
|
|
operations=[asdict(operation) for operation in preview.operations],
|
|
)
|