Files
2026-03-23 20:23:58 +08:00

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],
)