llm_registry_api/graphql/
mutation.rs

1//! GraphQL mutation resolvers
2//!
3//! This module implements all GraphQL mutation operations.
4
5use async_graphql::{Context, InputObject, Object, Result};
6use llm_registry_core::{
7    AssetId, Checksum, HashAlgorithm, StorageBackend, StorageLocation,
8};
9use llm_registry_service::{RegisterAssetRequest, ServiceRegistry, UpdateAssetRequest};
10use semver::Version;
11use std::sync::Arc;
12
13use super::types::{
14    GqlAsset, GqlAssetStatus, GqlAssetType, GqlDeleteResult, GqlRegisterResult, GqlUpdateResult,
15};
16use crate::auth::AuthUser;
17use crate::error::ApiError;
18
19/// Root Mutation type for GraphQL
20pub struct Mutation;
21
22/// Input for registering a new asset
23#[derive(InputObject)]
24pub struct RegisterAssetInput {
25    /// Asset type
26    pub asset_type: GqlAssetType,
27    /// Asset name
28    pub name: String,
29    /// Asset version (semver)
30    pub version: String,
31    /// Optional description
32    pub description: Option<String>,
33    /// Optional license
34    pub license: Option<String>,
35    /// Tags for categorization
36    #[graphql(default)]
37    pub tags: Vec<String>,
38    /// Key-value annotations
39    #[graphql(default)]
40    pub annotations: Vec<AnnotationInput>,
41    /// Storage path
42    pub storage_path: String,
43    /// Storage backend type (s3, gcs, azure, local)
44    #[graphql(default = "local")]
45    pub storage_backend: String,
46    /// Optional storage URI
47    pub storage_uri: Option<String>,
48    /// Checksum value (hex string)
49    pub checksum: String,
50    /// Checksum algorithm (SHA256, SHA3_256, BLAKE3)
51    #[graphql(default = "SHA256")]
52    pub checksum_algorithm: String,
53    /// File size in bytes
54    pub size_bytes: Option<u64>,
55    /// Content type
56    pub content_type: Option<String>,
57}
58
59/// Input for updating an asset
60#[derive(InputObject)]
61pub struct UpdateAssetInput {
62    /// Asset ID to update
63    pub asset_id: String,
64    /// New status
65    pub status: Option<GqlAssetStatus>,
66    /// New description
67    pub description: Option<String>,
68    /// New license
69    pub license: Option<String>,
70    /// Tags to add
71    #[graphql(default)]
72    pub add_tags: Vec<String>,
73    /// Tags to remove
74    #[graphql(default)]
75    pub remove_tags: Vec<String>,
76    /// Annotations to add/update
77    #[graphql(default)]
78    pub add_annotations: Vec<AnnotationInput>,
79    /// Annotation keys to remove
80    #[graphql(default)]
81    pub remove_annotations: Vec<String>,
82}
83
84/// Annotation key-value pair
85#[derive(InputObject)]
86pub struct AnnotationInput {
87    /// Annotation key
88    pub key: String,
89    /// Annotation value
90    pub value: String,
91}
92
93#[Object]
94impl Mutation {
95    /// Register a new asset
96    async fn register_asset(
97        &self,
98        ctx: &Context<'_>,
99        input: RegisterAssetInput,
100    ) -> Result<GqlRegisterResult> {
101        let services = ctx.data::<Arc<ServiceRegistry>>()?;
102
103        // Check authentication (optional - can be made required)
104        let _user = ctx.data_opt::<AuthUser>();
105
106        // Parse version
107        let version = Version::parse(&input.version)
108            .map_err(|e| ApiError::bad_request(format!("Invalid version: {}", e)))?;
109
110        // Parse hash algorithm
111        let algorithm = match input.checksum_algorithm.to_uppercase().as_str() {
112            "SHA256" => HashAlgorithm::SHA256,
113            "SHA3_256" | "SHA3-256" => HashAlgorithm::SHA3_256,
114            "BLAKE3" => HashAlgorithm::BLAKE3,
115            _ => return Err(ApiError::bad_request("Invalid checksum algorithm"))?
116        };
117
118        // Create storage backend
119        let backend = match input.storage_backend.to_lowercase().as_str() {
120            "filesystem" | "local" => StorageBackend::FileSystem {
121                base_path: "/var/lib/llm-registry".to_string(),
122            },
123            _ => StorageBackend::FileSystem {
124                base_path: "/var/lib/llm-registry".to_string(),
125            },
126        };
127
128        // Create storage location
129        let storage = StorageLocation {
130            backend,
131            path: input.storage_path,
132            uri: input.storage_uri,
133        };
134
135        // Create checksum
136        let checksum = Checksum {
137            algorithm,
138            value: input.checksum,
139        };
140
141        // Build registration request
142        let request = RegisterAssetRequest {
143            asset_type: input.asset_type.to_core(),
144            name: input.name,
145            version,
146            description: input.description,
147            license: input.license,
148            tags: input.tags,
149            annotations: input
150                .annotations
151                .into_iter()
152                .map(|a| (a.key, a.value))
153                .collect(),
154            storage,
155            checksum,
156            provenance: None,
157            dependencies: vec![],
158            size_bytes: input.size_bytes,
159            content_type: input.content_type,
160        };
161
162        let response = services
163            .registration()
164            .register_asset(request)
165            .await
166            .map_err(|e| ApiError::from(e))?;
167
168        Ok(GqlRegisterResult {
169            asset: GqlAsset(response.asset),
170            message: "Asset registered successfully".to_string(),
171        })
172    }
173
174    /// Update an existing asset
175    async fn update_asset(
176        &self,
177        ctx: &Context<'_>,
178        input: UpdateAssetInput,
179    ) -> Result<GqlUpdateResult> {
180        let services = ctx.data::<Arc<ServiceRegistry>>()?;
181
182        // Check authentication (optional - can be made required)
183        let _user = ctx.data_opt::<AuthUser>();
184
185        // Parse asset ID
186        let asset_id = input
187            .asset_id
188            .parse::<AssetId>()
189            .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
190
191        // Build update request
192        let request = UpdateAssetRequest {
193            asset_id,
194            status: input.status.map(|s| s.to_core()),
195            description: input.description,
196            license: input.license,
197            add_tags: input.add_tags,
198            remove_tags: input.remove_tags,
199            add_annotations: input
200                .add_annotations
201                .into_iter()
202                .map(|a| (a.key, a.value))
203                .collect(),
204            remove_annotations: input.remove_annotations,
205        };
206
207        let response = services
208            .registration()
209            .update_asset(request)
210            .await
211            .map_err(|e| ApiError::from(e))?;
212
213        Ok(GqlUpdateResult {
214            asset: GqlAsset(response.asset),
215            message: "Asset updated successfully".to_string(),
216        })
217    }
218
219    /// Delete an asset
220    async fn delete_asset(&self, ctx: &Context<'_>, id: String) -> Result<GqlDeleteResult> {
221        let services = ctx.data::<Arc<ServiceRegistry>>()?;
222
223        // Check authentication (optional - can be made required)
224        let _user = ctx.data_opt::<AuthUser>();
225
226        // Parse asset ID
227        let asset_id = id
228            .parse::<AssetId>()
229            .map_err(|e| ApiError::bad_request(format!("Invalid asset ID: {}", e)))?;
230
231        services
232            .registration()
233            .delete_asset(&asset_id)
234            .await
235            .map_err(|e| ApiError::from(e))?;
236
237        Ok(GqlDeleteResult {
238            asset_id: id,
239            message: "Asset deleted successfully".to_string(),
240        })
241    }
242}