pdt 0.3.5

Asset store API with relation graphs, tagging, full-text search, multi-instance SQLite tenancy, OIDC auth and Cedar authorization
//! Relation handlers

use axum::{
    extract::{Path, Query},
    Json,
};
use pep::oidc::types::JwtClaims;
use serde::Deserialize;
use utoipa::IntoParams;

use crate::auth::AuthenticatedUser;
use crate::cedar::enforcement::TenantState;
use crate::error::Result;
use crate::models::{CreateRelationRequest, Relation};
use crate::service::{relation_service::GraphNode, RelationService};

/// Query parameters for graph traversal
#[derive(Debug, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct TraverseParams {
    #[serde(default = "default_depth")]
    pub depth: u32,
}

fn default_depth() -> u32 {
    3
}

/// Extract JwtClaims from AuthenticatedUser.
///
/// Propagates the original `extra` map (role, groups, etc.) from the real JWT.
fn extract_claims(user: &AuthenticatedUser) -> JwtClaims {
    user.to_cedar_claims()
}

/// Create a new relation
#[utoipa::path(
    post,
    path = "/api/relations",
    request_body = CreateRelationRequest,
    responses(
        (status = 200, description = "Relation created successfully", body = Relation),
        (status = 404, description = "Source or target asset not found"),
    ),
    tag = "relations",
)]
pub async fn create_relation(
    tx: TenantState,
    user: AuthenticatedUser,
    Json(request): Json<CreateRelationRequest>,
) -> Result<Json<Relation>> {
    let user_id = &user.user_id;

    // Cedar enforcement: check permission on both assets
    if let Some(ref authorizer) = tx.authorizer() {
        let claims = extract_claims(&user);
        crate::cedar::enforcement::check_relation_permission(
            tx.services(),
            authorizer,
            &claims,
            "Relate",
            &request.from_asset_id,
            &request.to_asset_id,
        )
        .await?;
    }

    let relation = RelationService::create(tx.services(), request, user_id).await?;
    Ok(Json(relation))
}

/// Get relation by ID
#[utoipa::path(
    get,
    path = "/api/relations/{id}",
    params(
        ("id" = String, Path, description = "Relation ID"),
    ),
    responses(
        (status = 200, description = "Relation found", body = Relation),
        (status = 404, description = "Relation not found"),
    ),
    tag = "relations",
)]
pub async fn get_relation(
    tx: TenantState,
    Path(id): Path<String>,
) -> Result<Json<Relation>> {
    let relation = RelationService::get(tx.services(), &id).await?;
    Ok(Json(relation))
}

/// Delete a relation
#[utoipa::path(
    delete,
    path = "/api/relations/{id}",
    params(
        ("id" = String, Path, description = "Relation ID"),
    ),
    responses(
        (status = 200, description = "Relation deleted successfully"),
        (status = 404, description = "Relation not found"),
    ),
    tag = "relations",
)]
pub async fn delete_relation(
    tx: TenantState,
    user: AuthenticatedUser,
    Path(id): Path<String>,
) -> Result<Json<serde_json::Value>> {
    let user_id = &user.user_id;

    // Cedar enforcement: check permission on the relation's source asset
    if let Some(ref authorizer) = tx.authorizer() {
        let relation = RelationService::get(tx.services(), &id).await?;
        let claims = extract_claims(&user);
        crate::cedar::enforcement::check_asset_permission_by_id(
            tx.services(),
            authorizer,
            &claims,
            "Relate",
            &relation.from_asset_id,
        )
        .await?;
    }

    RelationService::delete(tx.services(), &id, user_id).await?;
    Ok(Json(serde_json::json!({ "deleted": true })))
}

/// Get all relations for an asset
#[utoipa::path(
    get,
    path = "/api/assets/{id}/relations",
    params(
        ("id" = String, Path, description = "Asset ID"),
    ),
    responses(
        (status = 200, description = "List of relations", body = [Relation]),
        (status = 404, description = "Asset not found"),
    ),
    tag = "relations",
)]
pub async fn get_asset_relations(
    tx: TenantState,
    Path(id): Path<String>,
) -> Result<Json<Vec<Relation>>> {
    let relations = RelationService::get_asset_relations(tx.services(), &id).await?;
    Ok(Json(relations))
}

/// Traverse the relationship graph from an asset
#[utoipa::path(
    get,
    path = "/api/assets/{id}/graph",
    params(
        ("id" = String, Path, description = "Asset ID"),
        TraverseParams,
    ),
    responses(
        (status = 200, description = "Graph traversal results", body = [GraphNode]),
        (status = 404, description = "Asset not found"),
    ),
    tag = "relations",
)]
pub async fn traverse_graph(
    tx: TenantState,
    Path(id): Path<String>,
    Query(params): Query<TraverseParams>,
) -> Result<Json<Vec<GraphNode>>> {
    let nodes = RelationService::traverse_graph(tx.services(), &id, params.depth).await?;
    Ok(Json(nodes))
}