llm_registry_api/graphql/
query.rs

1//! GraphQL query resolvers
2//!
3//! This module implements all GraphQL query operations.
4
5use async_graphql::{Context, Object, Result};
6use llm_registry_core::AssetId;
7use llm_registry_service::{SearchAssetsRequest, ServiceRegistry, SortField, SortOrder};
8use std::sync::Arc;
9
10use super::types::{GqlAsset, GqlAssetConnection, GqlAssetFilter, GqlDependencyNode};
11use crate::error::ApiError;
12
13/// Root Query type for GraphQL
14pub struct Query;
15
16#[Object]
17impl Query {
18    /// Get an asset by ID
19    async fn asset(&self, ctx: &Context<'_>, id: String) -> Result<Option<GqlAsset>> {
20        let services = ctx.data::<Arc<ServiceRegistry>>()?;
21
22        let asset_id = id
23            .parse::<AssetId>()
24            .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
25
26        let asset = services
27            .search()
28            .get_asset(&asset_id)
29            .await
30            .map_err(|e| ApiError::from(e))?;
31
32        Ok(asset.map(GqlAsset))
33    }
34
35    /// Search and list assets with optional filters
36    async fn assets(
37        &self,
38        ctx: &Context<'_>,
39        #[graphql(desc = "Filter criteria", default)] filter: Option<GqlAssetFilter>,
40        #[graphql(desc = "Number of items to return", default = 20)] limit: i64,
41        #[graphql(desc = "Number of items to skip", default = 0)] offset: i64,
42    ) -> Result<GqlAssetConnection> {
43        let services = ctx.data::<Arc<ServiceRegistry>>()?;
44
45        // Build search request
46        let mut search_request = SearchAssetsRequest {
47            text: None,
48            asset_types: vec![],
49            tags: vec![],
50            author: None,
51            storage_backend: None,
52            exclude_deprecated: true,
53            limit,
54            offset,
55            sort_by: SortField::CreatedAt,
56            sort_order: SortOrder::Descending,
57        };
58
59        // Apply filters if provided
60        if let Some(f) = filter {
61            if let Some(asset_type) = f.asset_type {
62                search_request.asset_types = vec![asset_type.to_core()];
63            }
64            if let Some(tags) = f.tags {
65                search_request.tags = tags;
66            }
67            search_request.text = f.name;
68            // Note: GqlAssetFilter has a status field but SearchAssetsRequest doesn't have one directly
69            // We can use exclude_deprecated based on status if needed
70            if let Some(status) = f.status {
71                use super::types::GqlAssetStatus;
72                search_request.exclude_deprecated = status != GqlAssetStatus::Deprecated;
73            }
74        }
75
76        let response = services
77            .search()
78            .search_assets(search_request)
79            .await
80            .map_err(|e| ApiError::from(e))?;
81
82        Ok(GqlAssetConnection {
83            nodes: response.assets.into_iter().map(GqlAsset).collect(),
84            total_count: response.total,
85            has_next_page: (response.offset + response.limit) < response.total,
86        })
87    }
88
89    /// Get all dependencies for an asset
90    async fn dependencies(
91        &self,
92        ctx: &Context<'_>,
93        #[graphql(desc = "Asset ID")] id: String,
94        #[graphql(desc = "Maximum depth to traverse (-1 for unlimited)", default = -1)]
95        max_depth: i32,
96    ) -> Result<Vec<GqlDependencyNode>> {
97        let services = ctx.data::<Arc<ServiceRegistry>>()?;
98
99        let asset_id = id
100            .parse::<AssetId>()
101            .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
102
103        let request = llm_registry_service::GetDependencyGraphRequest {
104            asset_id,
105            max_depth,
106        };
107
108        let response = services
109            .search()
110            .get_dependency_graph(request)
111            .await
112            .map_err(|e| ApiError::from(e))?;
113
114        Ok(response
115            .dependencies
116            .into_iter()
117            .map(GqlDependencyNode::from)
118            .collect())
119    }
120
121    /// Get all assets that depend on this asset (reverse dependencies)
122    async fn dependents(
123        &self,
124        ctx: &Context<'_>,
125        #[graphql(desc = "Asset ID")] id: String,
126    ) -> Result<Vec<GqlAsset>> {
127        let services = ctx.data::<Arc<ServiceRegistry>>()?;
128
129        let asset_id = id
130            .parse::<AssetId>()
131            .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
132
133        let dependents = services
134            .search()
135            .get_reverse_dependencies(&asset_id)
136            .await
137            .map_err(|e| ApiError::from(e))?;
138
139        Ok(dependents.into_iter().map(GqlAsset).collect())
140    }
141
142    /// Get all unique tags across all assets
143    async fn all_tags(&self, ctx: &Context<'_>) -> Result<Vec<String>> {
144        let services = ctx.data::<Arc<ServiceRegistry>>()?;
145
146        let tags = services
147            .search()
148            .list_all_tags()
149            .await
150            .map_err(|e| ApiError::from(e))?;
151
152        Ok(tags)
153    }
154
155    /// Health check - returns true if service is healthy
156    async fn health(&self, ctx: &Context<'_>) -> Result<bool> {
157        let services = ctx.data::<Arc<ServiceRegistry>>()?;
158
159        // Perform a simple database check
160        match services.search().list_all_tags().await {
161            Ok(_) => Ok(true),
162            Err(_) => Ok(false),
163        }
164    }
165
166    /// Get API version information
167    async fn version(&self) -> String {
168        env!("CARGO_PKG_VERSION").to_string()
169    }
170}