99 lines
3.0 KiB
Python
99 lines
3.0 KiB
Python
"""Artifact relationship APIs — explicit edges.
|
|
|
|
All endpoints are tenant-scoped. Cross-tenant edges can never be created
|
|
because both endpoint artifacts are validated against the tenant context.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
|
|
from asf_domain import (
|
|
ArtifactRelationship,
|
|
ArtifactRelationshipCreate,
|
|
RelationshipType,
|
|
)
|
|
from ...repositories import NotFoundError, DuplicateError, TenantMismatchError
|
|
from .deps import relationship_service
|
|
from ...services import RelationshipService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=ArtifactRelationship,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create a directed edge: source <relationship_type> target",
|
|
)
|
|
async def create_relationship(
|
|
body: ArtifactRelationshipCreate,
|
|
svc: RelationshipService = Depends(relationship_service),
|
|
) -> ArtifactRelationship:
|
|
try:
|
|
return await svc.create(body)
|
|
except DuplicateError as exc:
|
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
|
except TenantMismatchError as exc:
|
|
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
|
|
|
|
@router.delete(
|
|
"/{relationship_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete a relationship",
|
|
)
|
|
async def delete_relationship(
|
|
relationship_id: UUID,
|
|
svc: RelationshipService = Depends(relationship_service),
|
|
) -> None:
|
|
try:
|
|
await svc.delete(relationship_id)
|
|
except (NotFoundError, TenantMismatchError) as exc:
|
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
return None
|
|
|
|
|
|
@router.get(
|
|
"/from/{artifact_id}",
|
|
response_model=list[ArtifactRelationship],
|
|
summary="Outgoing edges: artifact → *",
|
|
)
|
|
async def list_outgoing(
|
|
artifact_id: UUID,
|
|
relationship_type: Optional[RelationshipType] = Query(None),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: RelationshipService = Depends(relationship_service),
|
|
) -> list[ArtifactRelationship]:
|
|
return await svc.outgoing(artifact_id, relationship_type=relationship_type, limit=limit)
|
|
|
|
|
|
@router.get(
|
|
"/to/{artifact_id}",
|
|
response_model=list[ArtifactRelationship],
|
|
summary="Incoming edges: * → artifact",
|
|
)
|
|
async def list_incoming(
|
|
artifact_id: UUID,
|
|
relationship_type: Optional[RelationshipType] = Query(None),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
svc: RelationshipService = Depends(relationship_service),
|
|
) -> list[ArtifactRelationship]:
|
|
return await svc.incoming(artifact_id, relationship_type=relationship_type, limit=limit)
|
|
|
|
|
|
@router.get(
|
|
"/for-artifact/{artifact_id}",
|
|
response_model=list[ArtifactRelationship],
|
|
summary="All edges touching an artifact (in + out)",
|
|
)
|
|
async def list_for_artifact(
|
|
artifact_id: UUID,
|
|
limit: int = Query(200, ge=1, le=1000),
|
|
svc: RelationshipService = Depends(relationship_service),
|
|
) -> list[ArtifactRelationship]:
|
|
return await svc.for_artifact(artifact_id, limit=limit)
|