This commit is contained in:
valenti
2026-09-08 07:21:34 -04:00
commit a4b224da8e
169 changed files with 27483 additions and 0 deletions
@@ -0,0 +1,98 @@
"""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)