add backend endpoint for publish readiness checks enforce publish gate contract before version publication make publish preconditions explicit and consistent for clients
268 lines
10 KiB
Python
268 lines
10 KiB
Python
from fastapi import APIRouter, Depends, Query
|
|
|
|
from app.core.config import settings
|
|
from app.repositories.audit import create_audit_event
|
|
from app.repositories.scheme_groups import clone_scheme_version_groups
|
|
from app.repositories.scheme_seats import clone_scheme_version_seats
|
|
from app.repositories.scheme_sectors import clone_scheme_version_sectors
|
|
from app.repositories.scheme_versions import (
|
|
count_scheme_versions,
|
|
create_next_scheme_version_from_current,
|
|
get_current_scheme_version,
|
|
list_scheme_versions,
|
|
)
|
|
from app.repositories.schemes import (
|
|
count_scheme_records,
|
|
get_scheme_record_by_scheme_id,
|
|
list_scheme_records,
|
|
rollback_scheme_to_version,
|
|
unpublish_scheme,
|
|
)
|
|
from app.schemas.scheme_registry import (
|
|
SchemeCurrentResponse,
|
|
SchemeDetailResponse,
|
|
SchemeListItem,
|
|
SchemeListResponse,
|
|
SchemePublishResponse,
|
|
SchemeRollbackRequest,
|
|
SchemeRollbackResponse,
|
|
)
|
|
from app.schemas.scheme_versions import (
|
|
SchemeVersionCreateResponse,
|
|
SchemeVersionListItem,
|
|
SchemeVersionListResponse,
|
|
)
|
|
from app.security.auth import require_api_key
|
|
from app.services.publish_service import publish_current_draft_scheme
|
|
from app.services.scheme_validation import build_scheme_validation_report
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes", response_model=SchemeListResponse)
|
|
async def get_schemes(
|
|
limit: int = Query(default=50, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
rows = await list_scheme_records(limit=limit, offset=offset)
|
|
total = await count_scheme_records()
|
|
|
|
items = [
|
|
SchemeListItem(
|
|
scheme_id=row.scheme_id,
|
|
source_upload_id=row.source_upload_id,
|
|
name=row.name,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
for row in rows
|
|
]
|
|
return SchemeListResponse(items=items, total=total)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}", response_model=SchemeDetailResponse)
|
|
async def get_scheme(scheme_id: str, role: str = Depends(require_api_key)):
|
|
row = await get_scheme_record_by_scheme_id(scheme_id)
|
|
return SchemeDetailResponse(
|
|
scheme_id=row.scheme_id,
|
|
source_upload_id=row.source_upload_id,
|
|
name=row.name,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/current", response_model=SchemeCurrentResponse)
|
|
async def get_scheme_current(scheme_id: str, role: str = Depends(require_api_key)):
|
|
scheme = await get_scheme_record_by_scheme_id(scheme_id)
|
|
version = await get_current_scheme_version(
|
|
scheme_id=scheme.scheme_id,
|
|
current_version_number=scheme.current_version_number,
|
|
)
|
|
return SchemeCurrentResponse(
|
|
scheme_id=version.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
version_number=version.version_number,
|
|
status=version.status,
|
|
normalized_storage_path=version.normalized_storage_path,
|
|
normalized_elements_count=version.normalized_elements_count,
|
|
normalized_seats_count=version.normalized_seats_count,
|
|
normalized_groups_count=version.normalized_groups_count,
|
|
normalized_sectors_count=version.normalized_sectors_count,
|
|
created_at=version.created_at.isoformat(),
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/versions", response_model=SchemeVersionListResponse)
|
|
async def get_scheme_versions(
|
|
scheme_id: str,
|
|
limit: int = Query(default=100, ge=1, le=200),
|
|
offset: int = Query(default=0, ge=0),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
rows = await list_scheme_versions(scheme_id=scheme_id, limit=limit, offset=offset)
|
|
total = await count_scheme_versions(scheme_id=scheme_id)
|
|
|
|
items = [
|
|
SchemeVersionListItem(
|
|
scheme_version_id=row.scheme_version_id,
|
|
scheme_id=row.scheme_id,
|
|
version_number=row.version_number,
|
|
status=row.status,
|
|
normalized_storage_path=row.normalized_storage_path,
|
|
normalized_elements_count=row.normalized_elements_count,
|
|
normalized_seats_count=row.normalized_seats_count,
|
|
normalized_groups_count=row.normalized_groups_count,
|
|
normalized_sectors_count=row.normalized_sectors_count,
|
|
created_at=row.created_at.isoformat(),
|
|
)
|
|
for row in rows
|
|
]
|
|
return SchemeVersionListResponse(items=items, total=total)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/versions", response_model=SchemeVersionCreateResponse)
|
|
async def create_next_scheme_version_endpoint(
|
|
scheme_id: str,
|
|
expected_current_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
current_scheme = await get_scheme_record_by_scheme_id(scheme_id)
|
|
current_version = await get_current_scheme_version(
|
|
scheme_id=current_scheme.scheme_id,
|
|
current_version_number=current_scheme.current_version_number,
|
|
)
|
|
|
|
if expected_current_scheme_version_id and expected_current_scheme_version_id != current_version.scheme_version_id:
|
|
from fastapi import HTTPException, status
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail={
|
|
"code": "stale_current_version",
|
|
"message": "Current scheme version changed. Reload scheme state before creating a new version.",
|
|
"expected_scheme_version_id": expected_current_scheme_version_id,
|
|
"actual_scheme_version_id": current_version.scheme_version_id,
|
|
},
|
|
)
|
|
|
|
new_version = await create_next_scheme_version_from_current(scheme_id)
|
|
|
|
await clone_scheme_version_sectors(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
await clone_scheme_version_groups(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
await clone_scheme_version_seats(
|
|
source_scheme_version_id=current_version.scheme_version_id,
|
|
target_scheme_version_id=new_version.scheme_version_id,
|
|
)
|
|
|
|
await create_audit_event(
|
|
scheme_id=scheme_id,
|
|
event_type="scheme.version.created",
|
|
object_type="scheme_version",
|
|
object_ref=new_version.scheme_version_id,
|
|
details={
|
|
"source_scheme_version_id": current_version.scheme_version_id,
|
|
"version_number": new_version.version_number,
|
|
"normalized_storage_path": new_version.normalized_storage_path,
|
|
},
|
|
)
|
|
|
|
return SchemeVersionCreateResponse(
|
|
scheme_id=new_version.scheme_id,
|
|
scheme_version_id=new_version.scheme_version_id,
|
|
version_number=new_version.version_number,
|
|
status=new_version.status,
|
|
normalized_storage_path=new_version.normalized_storage_path,
|
|
)
|
|
|
|
|
|
@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/publish/validation")
|
|
async def get_publish_validation(scheme_id: str, role: str = Depends(require_api_key)):
|
|
scheme = await get_scheme_record_by_scheme_id(scheme_id)
|
|
version = await get_current_scheme_version(
|
|
scheme_id=scheme.scheme_id,
|
|
current_version_number=scheme.current_version_number,
|
|
)
|
|
report = await build_scheme_validation_report(
|
|
scheme_id=scheme.scheme_id,
|
|
scheme_version_id=version.scheme_version_id,
|
|
)
|
|
return {
|
|
"scheme_id": scheme.scheme_id,
|
|
"scheme_version_id": version.scheme_version_id,
|
|
"report": report,
|
|
}
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/publish")
|
|
async def publish_scheme_endpoint(
|
|
scheme_id: str,
|
|
expected_scheme_version_id: str | None = Query(default=None),
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
return await publish_current_draft_scheme(
|
|
scheme_id=scheme_id,
|
|
expected_scheme_version_id=expected_scheme_version_id,
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/unpublish", response_model=SchemePublishResponse)
|
|
async def unpublish_scheme_endpoint(scheme_id: str, role: str = Depends(require_api_key)):
|
|
row = await unpublish_scheme(scheme_id)
|
|
await create_audit_event(
|
|
scheme_id=row.scheme_id,
|
|
event_type="scheme.unpublished",
|
|
object_type="scheme",
|
|
object_ref=row.scheme_id,
|
|
details={"current_version_number": row.current_version_number, "status": row.status},
|
|
)
|
|
return SchemePublishResponse(
|
|
scheme_id=row.scheme_id,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
)
|
|
|
|
|
|
@router.post(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/rollback", response_model=SchemeRollbackResponse)
|
|
async def rollback_scheme_endpoint(
|
|
scheme_id: str,
|
|
payload: SchemeRollbackRequest,
|
|
role: str = Depends(require_api_key),
|
|
):
|
|
row = await rollback_scheme_to_version(
|
|
scheme_id=scheme_id,
|
|
target_version_number=payload.target_version_number,
|
|
)
|
|
await create_audit_event(
|
|
scheme_id=row.scheme_id,
|
|
event_type="scheme.rolled_back",
|
|
object_type="scheme_version",
|
|
object_ref=str(payload.target_version_number),
|
|
details={"current_version_number": row.current_version_number, "status": row.status},
|
|
)
|
|
return SchemeRollbackResponse(
|
|
scheme_id=row.scheme_id,
|
|
status=row.status,
|
|
current_version_number=row.current_version_number,
|
|
published_at=row.published_at.isoformat() if row.published_at else None,
|
|
)
|