feat(records): implement native eAkte vertical

This commit is contained in:
2026-08-06 01:43:23 +02:00
parent 59860f7ac0
commit f316523a1d
28 changed files with 5700 additions and 80 deletions
+274
View File
@@ -0,0 +1,274 @@
from __future__ import annotations
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.records import RecordFilingRequest, RecordSourceLocator
from govoplan_core.db.session import get_session
from govoplan_records.backend.manifest import ADMIN_SCOPE, READ_SCOPE, WRITE_SCOPE
from govoplan_records.backend.schemas import (
FilePlanNodeWriteRequest,
RecordCatalogResponse,
RecordClassWriteRequest,
RecordCreateRequest,
RecordDetailResponse,
RecordItemCreateRequest,
RecordListResponse,
RecordSourceProviderResponse,
RecordUpdateRequest,
RecordVolumeCreateRequest,
)
from govoplan_records.backend.service import (
RecordConflictError,
RecordNotFoundError,
RecordSourceUnavailableError,
RecordStoreError,
SqlRecordRegistry,
)
def create_router(registry: object | None = None) -> APIRouter:
router = APIRouter(prefix="/records", tags=["records"])
records = SqlRecordRegistry(registry)
@router.get("/catalog", response_model=RecordCatalogResponse)
def api_catalog(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecordCatalogResponse:
_require(principal, READ_SCOPE)
return RecordCatalogResponse(**records.catalog(session, principal))
@router.post(
"/catalog/file-plan",
response_model=dict[str, Any],
status_code=status.HTTP_201_CREATED,
)
def api_write_file_plan_node(
payload: FilePlanNodeWriteRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, ADMIN_SCOPE)
return _write(
session,
lambda: records.write_file_plan_node(
session, principal, payload=payload.model_dump(mode="python")
),
)
@router.post(
"/catalog/classes",
response_model=dict[str, Any],
status_code=status.HTTP_201_CREATED,
)
def api_write_record_class(
payload: RecordClassWriteRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, ADMIN_SCOPE)
return _write(
session,
lambda: records.write_record_class(
session, principal, payload=payload.model_dump(mode="python")
),
)
@router.get("/sources", response_model=RecordSourceProviderResponse)
def api_source_providers(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecordSourceProviderResponse:
_require(principal, WRITE_SCOPE)
return RecordSourceProviderResponse(
providers=records.source_providers(session, principal)
)
@router.get("", response_model=RecordListResponse)
def api_list_records(
query: str | None = Query(default=None, max_length=500),
record_state: str | None = Query(default=None, alias="state", max_length=40),
class_id: str | None = Query(default=None, max_length=255),
file_plan_node_id: str | None = Query(default=None, max_length=255),
offset: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=200),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecordListResponse:
_require(principal, READ_SCOPE)
items, total = records.list_records(
session,
principal,
query=query,
state=record_state,
class_id=class_id,
file_plan_node_id=file_plan_node_id,
offset=offset,
limit=limit,
)
return RecordListResponse(
records=items, total=total, offset=offset, limit=limit
)
@router.post("", response_model=dict[str, Any], status_code=status.HTTP_201_CREATED)
def api_create_record(
payload: RecordCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, WRITE_SCOPE)
return _write(
session,
lambda: records.create_record(
session, principal, payload=payload.model_dump(mode="python")
),
)
@router.get("/{record_id}", response_model=RecordDetailResponse)
def api_get_record(
record_id: str,
revision: int | None = Query(default=None, ge=1),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> RecordDetailResponse:
_require(principal, READ_SCOPE)
try:
return RecordDetailResponse(
**records.get_record(
session, principal, record_id=record_id, revision=revision
)
)
except RecordStoreError as exc:
raise _http_error(exc) from exc
@router.patch("/{record_id}", response_model=dict[str, Any])
def api_update_record(
record_id: str,
payload: RecordUpdateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, WRITE_SCOPE)
return _write(
session,
lambda: records.update_record(
session,
principal,
record_id=record_id,
payload=payload.model_dump(mode="python", exclude_unset=True),
),
)
@router.post(
"/{record_id}/volumes",
response_model=dict[str, Any],
status_code=status.HTTP_201_CREATED,
)
def api_create_volume(
record_id: str,
payload: RecordVolumeCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, WRITE_SCOPE)
return _write(
session,
lambda: records.create_volume(
session,
principal,
record_id=record_id,
payload=payload.model_dump(mode="python"),
),
)
@router.post(
"/{record_id}/items",
response_model=dict[str, Any],
status_code=status.HTTP_201_CREATED,
)
def api_file_item(
record_id: str,
payload: RecordItemCreateRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, Any]:
_require(principal, WRITE_SCOPE)
request = RecordFilingRequest(
tenant_id=principal.tenant_id,
record_id=record_id,
source=RecordSourceLocator(
tenant_id=principal.tenant_id,
source_module=payload.source.source_module,
resource_type=payload.source.resource_type,
resource_id=payload.source.resource_id,
source_revision=payload.source.source_revision,
metadata=payload.source.metadata,
),
purpose=payload.purpose,
filing_reason=payload.filing_reason,
idempotency_key=payload.idempotency_key,
volume_id=payload.volume_id,
relationship=payload.relationship,
institutional_context=payload.institutional_context,
metadata=payload.metadata,
)
def operation() -> dict[str, Any]:
result = records.file(session, principal, request=request)
return {
"record_id": result.record_id,
"item_id": result.item_id,
"sequence": result.sequence,
"filed_at": result.filed_at,
"replayed": result.replayed,
"source": {
"source_module": result.source.locator.source_module,
"resource_type": result.source.locator.resource_type,
"resource_id": result.source.locator.resource_id,
"source_revision": result.source.locator.source_revision,
"label": result.source.label,
},
}
return _write(session, operation)
return router
def _require(principal: ApiPrincipal, scope: str) -> None:
if not has_scope(principal, scope):
raise HTTPException(status_code=403, detail=f"Missing scope: {scope}")
def _write(session: Session, operation):
try:
result = operation()
session.commit()
return result
except (RecordStoreError, IntegrityError) as exc:
session.rollback()
if isinstance(exc, IntegrityError):
raise HTTPException(
status_code=409, detail="The record write conflicts with existing data."
) from exc
raise _http_error(exc) from exc
def _http_error(exc: RecordStoreError) -> HTTPException:
if isinstance(exc, RecordNotFoundError):
code = 404
elif isinstance(exc, RecordConflictError):
code = 409
elif isinstance(exc, RecordSourceUnavailableError):
code = 503
else:
code = 422
return HTTPException(status_code=code, detail=str(exc))
__all__ = ["create_router"]