Skip to main content

this/links/
handlers.rs

1//! HTTP handlers for link operations
2//!
3//! This module provides generic handlers that work with any entity types.
4//! All handlers are completely entity-agnostic.
5
6use axum::{
7    Json,
8    extract::{Path, Query, State},
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::config::LinksConfig;
20use crate::core::events::{EventBus, FrameworkEvent, LinkEvent};
21use crate::core::extractors::{
22    DirectLinkExtractor, ExtractorError, LinkExtractor, RecursiveLinkExtractor,
23};
24use crate::core::{
25    EntityCreator, EntityFetcher, LinkDefinition, LinkService,
26    link::LinkEntity,
27    query::{PaginationMeta, QueryParams},
28};
29use crate::links::registry::{LinkDirection, LinkRouteRegistry};
30
31/// Application state shared across handlers
32#[derive(Clone)]
33pub struct AppState {
34    pub link_service: Arc<dyn LinkService>,
35    pub config: Arc<LinksConfig>,
36    pub registry: Arc<LinkRouteRegistry>,
37    /// Entity fetchers for enriching links with full entity data
38    pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
39    /// Entity creators for creating new entities with automatic linking
40    pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
41    /// Optional event bus for publishing real-time events
42    pub event_bus: Option<Arc<EventBus>>,
43}
44
45impl AppState {
46    /// Publish an event to the event bus (if configured)
47    ///
48    /// This is non-blocking and fire-and-forget. If there are no subscribers
49    /// or no event bus configured, the event is silently dropped.
50    pub fn publish_event(&self, event: FrameworkEvent) {
51        if let Some(ref bus) = self.event_bus {
52            bus.publish(event);
53        }
54    }
55
56    /// Get the authorization policy for a link operation
57    pub fn get_link_auth_policy(
58        link_definition: &LinkDefinition,
59        operation: &str,
60    ) -> Option<String> {
61        link_definition.auth.as_ref().map(|auth| match operation {
62            "list" => auth.list.clone(),
63            "get" => auth.get.clone(),
64            "create" => auth.create.clone(),
65            "update" => auth.update.clone(),
66            "delete" => auth.delete.clone(),
67            _ => "authenticated".to_string(),
68        })
69    }
70}
71
72/// Response for list links endpoint
73#[derive(Debug, Serialize)]
74pub struct ListLinksResponse {
75    pub links: Vec<LinkEntity>,
76    pub count: usize,
77    pub link_type: String,
78    pub direction: String,
79    pub description: Option<String>,
80}
81
82/// Link with full entity data instead of just references
83#[derive(Debug, Serialize)]
84pub struct EnrichedLink {
85    /// Unique identifier for this link
86    pub id: Uuid,
87
88    /// Entity type
89    #[serde(rename = "type")]
90    pub entity_type: String,
91
92    /// The type of relationship (e.g., "has_invoice", "payment")
93    pub link_type: String,
94
95    /// Source entity ID
96    pub source_id: Uuid,
97
98    /// Target entity ID
99    pub target_id: Uuid,
100
101    /// Full source entity as JSON (omitted when querying from source)
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub source: Option<serde_json::Value>,
104
105    /// Full target entity as JSON (omitted when querying from target)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub target: Option<serde_json::Value>,
108
109    /// Optional metadata for the relationship
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub metadata: Option<serde_json::Value>,
112
113    /// When this link was created
114    pub created_at: DateTime<Utc>,
115
116    /// When this link was last updated
117    pub updated_at: DateTime<Utc>,
118
119    /// Status
120    pub status: String,
121}
122
123/// Response for enriched list links endpoint (legacy, without pagination)
124#[derive(Debug, Serialize)]
125pub struct EnrichedListLinksResponse {
126    pub links: Vec<EnrichedLink>,
127    pub count: usize,
128    pub link_type: String,
129    pub direction: String,
130    pub description: Option<String>,
131}
132
133/// Paginated response for enriched list links endpoint
134#[derive(Debug, Serialize)]
135pub struct PaginatedEnrichedLinksResponse {
136    pub data: Vec<EnrichedLink>,
137    pub pagination: PaginationMeta,
138    pub link_type: String,
139    pub direction: String,
140    pub description: Option<String>,
141}
142
143/// Request body for creating a link between existing entities
144#[derive(Debug, Deserialize)]
145pub struct CreateLinkRequest {
146    pub metadata: Option<serde_json::Value>,
147}
148
149/// Request body for creating a new linked entity
150#[derive(Debug, Deserialize)]
151pub struct CreateLinkedEntityRequest {
152    pub entity: serde_json::Value,
153    pub metadata: Option<serde_json::Value>,
154}
155
156/// Context for link enrichment
157#[derive(Debug, Clone, Copy)]
158pub enum EnrichmentContext {
159    /// Query from source entity - only target entities are included
160    FromSource,
161    /// Query from target entity - only source entities are included
162    FromTarget,
163    /// Direct link access - both source and target entities are included
164    DirectLink,
165}
166
167/// List links using named routes (forward or reverse) - WITH PAGINATION
168///
169/// GET /{entity_type}/{entity_id}/{route_name}
170pub async fn list_links(
171    State(state): State<AppState>,
172    Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
173    Query(params): Query<QueryParams>,
174) -> Result<Json<PaginatedEnrichedLinksResponse>, ExtractorError> {
175    let extractor = LinkExtractor::from_path_and_registry(
176        (entity_type_plural, entity_id, route_name),
177        &state.registry,
178        &state.config,
179    )?;
180
181    // Query links based on direction
182    let links = match extractor.direction {
183        LinkDirection::Forward => state
184            .link_service
185            .find_by_source(
186                &extractor.entity_id,
187                Some(&extractor.link_definition.link_type),
188                Some(&extractor.link_definition.target_type),
189            )
190            .await
191            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
192        LinkDirection::Reverse => state
193            .link_service
194            .find_by_target(
195                &extractor.entity_id,
196                Some(&extractor.link_definition.link_type),
197                Some(&extractor.link_definition.source_type),
198            )
199            .await
200            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
201    };
202
203    // Determine enrichment context based on direction
204    let context = match extractor.direction {
205        LinkDirection::Forward => EnrichmentContext::FromSource,
206        LinkDirection::Reverse => EnrichmentContext::FromTarget,
207    };
208
209    // Enrich ALL links with full entity data first
210    let mut all_enriched =
211        enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
212
213    // Apply filters if provided
214    if let Some(filter_value) = params.filter_value() {
215        all_enriched = apply_link_filters(all_enriched, &filter_value);
216    }
217
218    let total = all_enriched.len();
219
220    // Apply pagination (ALWAYS paginate for links)
221    let page = params.page();
222    let limit = params.limit();
223    let start = (page - 1) * limit;
224
225    let paginated_links: Vec<EnrichedLink> =
226        all_enriched.into_iter().skip(start).take(limit).collect();
227
228    Ok(Json(PaginatedEnrichedLinksResponse {
229        data: paginated_links,
230        pagination: PaginationMeta::new(page, limit, total),
231        link_type: extractor.link_definition.link_type,
232        direction: format!("{:?}", extractor.direction),
233        description: extractor.link_definition.description,
234    }))
235}
236
237/// Helper function to enrich links with full entity data
238async fn enrich_links_with_entities(
239    state: &AppState,
240    links: Vec<LinkEntity>,
241    context: EnrichmentContext,
242    link_definition: &LinkDefinition,
243) -> Result<Vec<EnrichedLink>, ExtractorError> {
244    let mut enriched = Vec::new();
245
246    for link in links {
247        // Fetch source entity only if needed
248        let source_entity = match context {
249            EnrichmentContext::FromSource => None,
250            EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
251                // Fetch source entity using the type from link definition
252                fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
253                    .await
254                    .ok()
255            }
256        };
257
258        // Fetch target entity only if needed
259        let target_entity = match context {
260            EnrichmentContext::FromTarget => None,
261            EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
262                // Fetch target entity using the type from link definition
263                fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
264                    .await
265                    .ok()
266            }
267        };
268
269        enriched.push(EnrichedLink {
270            id: link.id,
271            entity_type: link.entity_type,
272            link_type: link.link_type,
273            source_id: link.source_id,
274            target_id: link.target_id,
275            source: source_entity,
276            target: target_entity,
277            metadata: link.metadata,
278            created_at: link.created_at,
279            updated_at: link.updated_at,
280            status: link.status,
281        });
282    }
283
284    Ok(enriched)
285}
286
287/// Fetch an entity dynamically by type
288async fn fetch_entity_by_type(
289    state: &AppState,
290    entity_type: &str,
291    entity_id: &Uuid,
292) -> Result<serde_json::Value, ExtractorError> {
293    let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
294        ExtractorError::JsonError(format!(
295            "No entity fetcher registered for type: {}",
296            entity_type
297        ))
298    })?;
299
300    fetcher
301        .fetch_as_json(entity_id)
302        .await
303        .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
304}
305
306/// Apply filtering to enriched links based on query parameters
307///
308/// Supports filtering on:
309/// - link fields (id, link_type, source_id, target_id, status, metadata)
310/// - nested entity fields (source.*, target.*)
311fn apply_link_filters(enriched_links: Vec<EnrichedLink>, filter: &Value) -> Vec<EnrichedLink> {
312    if filter.is_null() || !filter.is_object() {
313        return enriched_links;
314    }
315
316    let filter_obj = filter.as_object().unwrap();
317
318    enriched_links
319        .into_iter()
320        .filter(|link| {
321            let mut matches = true;
322
323            // Convert link to JSON for easy filtering
324            let link_json = match serde_json::to_value(link) {
325                Ok(v) => v,
326                Err(_) => return false,
327            };
328
329            for (key, value) in filter_obj.iter() {
330                // Check if the field exists in the link or in nested entities
331                let field_value = get_nested_value(&link_json, key);
332
333                match field_value {
334                    Some(field_val) => {
335                        // Simple equality match for now
336                        if field_val != *value {
337                            matches = false;
338                            break;
339                        }
340                    }
341                    None => {
342                        matches = false;
343                        break;
344                    }
345                }
346            }
347
348            matches
349        })
350        .collect()
351}
352
353/// Get a nested value from JSON using dot notation
354/// E.g., "source.name" or "target.amount"
355fn get_nested_value(json: &Value, key: &str) -> Option<Value> {
356    let parts: Vec<&str> = key.split('.').collect();
357
358    match parts.len() {
359        1 => json.get(key).cloned(),
360        2 => {
361            let (parent, child) = (parts[0], parts[1]);
362            json.get(parent).and_then(|v| v.get(child)).cloned()
363        }
364        _ => None,
365    }
366}
367
368/// Get a specific link by ID
369///
370/// GET /links/{link_id}
371pub async fn get_link(
372    State(state): State<AppState>,
373    Path(link_id): Path<Uuid>,
374) -> Result<Response, ExtractorError> {
375    let link = state
376        .link_service
377        .get(&link_id)
378        .await
379        .map_err(|e| ExtractorError::JsonError(e.to_string()))?
380        .ok_or(ExtractorError::LinkNotFound)?;
381
382    // Find the link definition from config
383    let link_definition = state
384        .config
385        .links
386        .iter()
387        .find(|def| def.link_type == link.link_type)
388        .ok_or_else(|| {
389            ExtractorError::JsonError(format!(
390                "No link definition found for link_type: {}",
391                link.link_type
392            ))
393        })?;
394
395    // Enrich with both source and target entities
396    let enriched_links = enrich_links_with_entities(
397        &state,
398        vec![link],
399        EnrichmentContext::DirectLink,
400        link_definition,
401    )
402    .await?;
403
404    let enriched_link = enriched_links
405        .into_iter()
406        .next()
407        .ok_or(ExtractorError::LinkNotFound)?;
408
409    Ok(Json(enriched_link).into_response())
410}
411
412/// Get a specific link by source, route_name, and target
413///
414/// GET /{source_type}/{source_id}/{route_name}/{target_id}
415pub async fn get_link_by_route(
416    State(state): State<AppState>,
417    Path((source_type_plural, source_id, route_name, target_id)): Path<(
418        String,
419        Uuid,
420        String,
421        Uuid,
422    )>,
423) -> Result<Response, ExtractorError> {
424    let extractor = DirectLinkExtractor::from_path(
425        (source_type_plural, source_id, route_name, target_id),
426        &state.registry,
427        &state.config,
428    )?;
429
430    // Find the specific link based on direction
431    let existing_links = match extractor.direction {
432        LinkDirection::Forward => {
433            // Forward: search by source_id in URL
434            state
435                .link_service
436                .find_by_source(
437                    &extractor.source_id,
438                    Some(&extractor.link_definition.link_type),
439                    Some(&extractor.target_type),
440                )
441                .await
442                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
443        }
444        LinkDirection::Reverse => {
445            // Reverse: search by target_id in URL (which is the actual source in DB)
446            state
447                .link_service
448                .find_by_source(
449                    &extractor.target_id,
450                    Some(&extractor.link_definition.link_type),
451                    Some(&extractor.source_type),
452                )
453                .await
454                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
455        }
456    };
457
458    let link = existing_links
459        .into_iter()
460        .find(|link| match extractor.direction {
461            LinkDirection::Forward => link.target_id == extractor.target_id,
462            LinkDirection::Reverse => link.target_id == extractor.source_id,
463        })
464        .ok_or(ExtractorError::LinkNotFound)?;
465
466    // Enrich with both source and target entities
467    let enriched_links = enrich_links_with_entities(
468        &state,
469        vec![link],
470        EnrichmentContext::DirectLink,
471        &extractor.link_definition,
472    )
473    .await?;
474
475    let enriched_link = enriched_links
476        .into_iter()
477        .next()
478        .ok_or(ExtractorError::LinkNotFound)?;
479
480    Ok(Json(enriched_link).into_response())
481}
482
483/// Create a link between two existing entities
484///
485/// POST /{source_type}/{source_id}/{route_name}/{target_id}
486/// Body: { "metadata": {...} }
487pub async fn create_link(
488    State(state): State<AppState>,
489    Path((source_type_plural, source_id, route_name, target_id)): Path<(
490        String,
491        Uuid,
492        String,
493        Uuid,
494    )>,
495    Json(payload): Json<CreateLinkRequest>,
496) -> Result<Response, ExtractorError> {
497    let extractor = DirectLinkExtractor::from_path(
498        (source_type_plural, source_id, route_name, target_id),
499        &state.registry,
500        &state.config,
501    )?;
502
503    // Create the link between existing entities
504    let link = LinkEntity::new(
505        extractor.link_definition.link_type,
506        extractor.source_id,
507        extractor.target_id,
508        payload.metadata,
509    );
510
511    let created_link = state
512        .link_service
513        .create(link)
514        .await
515        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
516
517    // Emit link created event
518    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
519        link_type: created_link.link_type.clone(),
520        link_id: created_link.id,
521        source_id: created_link.source_id,
522        target_id: created_link.target_id,
523        metadata: created_link.metadata.clone(),
524    }));
525
526    Ok((StatusCode::CREATED, Json(created_link)).into_response())
527}
528
529/// Create a new entity and link it to the source
530///
531/// POST /{source_type}/{source_id}/{route_name}
532/// Body: { "entity": {...entity fields...}, "metadata": {...link metadata...} }
533pub async fn create_linked_entity(
534    State(state): State<AppState>,
535    Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
536    Json(payload): Json<CreateLinkedEntityRequest>,
537) -> Result<Response, ExtractorError> {
538    let extractor = LinkExtractor::from_path_and_registry(
539        (source_type_plural.clone(), source_id, route_name.clone()),
540        &state.registry,
541        &state.config,
542    )?;
543
544    // Determine source and target based on direction
545    let (source_entity_id, target_entity_type) = match extractor.direction {
546        LinkDirection::Forward => {
547            // Forward: source is the entity in the URL, target is the new entity
548            (extractor.entity_id, &extractor.link_definition.target_type)
549        }
550        LinkDirection::Reverse => {
551            // Reverse: target is the entity in the URL, source is the new entity
552            (extractor.entity_id, &extractor.link_definition.source_type)
553        }
554    };
555
556    // Get the entity creator for the target type
557    let entity_creator = state
558        .entity_creators
559        .get(target_entity_type)
560        .ok_or_else(|| {
561            ExtractorError::JsonError(format!(
562                "No entity creator registered for type: {}",
563                target_entity_type
564            ))
565        })?;
566
567    // Create the new entity
568    let created_entity = entity_creator
569        .create_from_json(payload.entity)
570        .await
571        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
572
573    // Extract the ID from the created entity
574    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
575        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
576    })?;
577    let target_entity_id = Uuid::parse_str(target_entity_id)
578        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
579
580    // Create the link based on direction
581    let link = match extractor.direction {
582        LinkDirection::Forward => {
583            // Forward: source -> target (new entity)
584            LinkEntity::new(
585                extractor.link_definition.link_type,
586                source_entity_id,
587                target_entity_id,
588                payload.metadata,
589            )
590        }
591        LinkDirection::Reverse => {
592            // Reverse: source (new entity) -> target
593            LinkEntity::new(
594                extractor.link_definition.link_type,
595                target_entity_id,
596                source_entity_id,
597                payload.metadata,
598            )
599        }
600    };
601
602    let created_link = state
603        .link_service
604        .create(link)
605        .await
606        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
607
608    // Emit entity created event
609    state.publish_event(FrameworkEvent::Entity(
610        crate::core::events::EntityEvent::Created {
611            entity_type: target_entity_type.clone(),
612            entity_id: target_entity_id,
613            data: created_entity.clone(),
614        },
615    ));
616
617    // Emit link created event
618    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
619        link_type: created_link.link_type.clone(),
620        link_id: created_link.id,
621        source_id: created_link.source_id,
622        target_id: created_link.target_id,
623        metadata: created_link.metadata.clone(),
624    }));
625
626    // Return both the created entity and the link
627    let response = serde_json::json!({
628        "entity": created_entity,
629        "link": created_link,
630    });
631
632    Ok((StatusCode::CREATED, Json(response)).into_response())
633}
634
635/// Update a link's metadata using route name
636///
637/// PUT/PATCH /{source_type}/{source_id}/{route_name}/{target_id}
638pub async fn update_link(
639    State(state): State<AppState>,
640    Path((source_type_plural, source_id, route_name, target_id)): Path<(
641        String,
642        Uuid,
643        String,
644        Uuid,
645    )>,
646    Json(payload): Json<CreateLinkRequest>,
647) -> Result<Response, ExtractorError> {
648    let extractor = DirectLinkExtractor::from_path(
649        (source_type_plural, source_id, route_name, target_id),
650        &state.registry,
651        &state.config,
652    )?;
653
654    // Find the existing link
655    let existing_links = state
656        .link_service
657        .find_by_source(
658            &extractor.source_id,
659            Some(&extractor.link_definition.link_type),
660            Some(&extractor.target_type),
661        )
662        .await
663        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
664
665    let mut existing_link = existing_links
666        .into_iter()
667        .find(|link| link.target_id == extractor.target_id)
668        .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
669
670    // Update metadata
671    existing_link.metadata = payload.metadata;
672    existing_link.touch();
673
674    // Save the updated link
675    let link_id = existing_link.id;
676    let updated_link = state
677        .link_service
678        .update(&link_id, existing_link)
679        .await
680        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
681
682    Ok(Json(updated_link).into_response())
683}
684
685/// Delete a link using route name
686///
687/// DELETE /{source_type}/{source_id}/{route_name}/{target_id}
688pub async fn delete_link(
689    State(state): State<AppState>,
690    Path((source_type_plural, source_id, route_name, target_id)): Path<(
691        String,
692        Uuid,
693        String,
694        Uuid,
695    )>,
696) -> Result<Response, ExtractorError> {
697    let extractor = DirectLinkExtractor::from_path(
698        (source_type_plural, source_id, route_name, target_id),
699        &state.registry,
700        &state.config,
701    )?;
702
703    // Find the existing link first
704    let existing_links = state
705        .link_service
706        .find_by_source(
707            &extractor.source_id,
708            Some(&extractor.link_definition.link_type),
709            Some(&extractor.target_type),
710        )
711        .await
712        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
713
714    let existing_link = existing_links
715        .into_iter()
716        .find(|link| link.target_id == extractor.target_id)
717        .ok_or(ExtractorError::LinkNotFound)?;
718
719    // Delete the link by its ID
720    state
721        .link_service
722        .delete(&existing_link.id)
723        .await
724        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
725
726    // Emit link deleted event
727    state.publish_event(FrameworkEvent::Link(LinkEvent::Deleted {
728        link_type: existing_link.link_type.clone(),
729        link_id: existing_link.id,
730        source_id: existing_link.source_id,
731        target_id: existing_link.target_id,
732    }));
733
734    Ok(StatusCode::NO_CONTENT.into_response())
735}
736
737/// Response for introspection endpoint
738#[derive(Debug, Serialize)]
739pub struct IntrospectionResponse {
740    pub entity_type: String,
741    pub entity_id: Uuid,
742    pub available_routes: Vec<RouteDescription>,
743}
744
745/// Description of an available route
746#[derive(Debug, Serialize)]
747pub struct RouteDescription {
748    pub path: String,
749    pub method: String,
750    pub link_type: String,
751    pub direction: String,
752    pub connected_to: String,
753    pub description: Option<String>,
754}
755
756/// Introspection: List all available link routes for an entity
757///
758/// GET /{entity_type}/{entity_id}/links
759pub async fn list_available_links(
760    State(state): State<AppState>,
761    Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
762) -> Result<Json<IntrospectionResponse>, ExtractorError> {
763    // Convert plural to singular
764    let entity_type = state
765        .config
766        .entities
767        .iter()
768        .find(|e| e.plural == entity_type_plural)
769        .map(|e| e.singular.clone())
770        .unwrap_or_else(|| entity_type_plural.clone());
771
772    // Get all routes for this entity type
773    let routes = state.registry.list_routes_for_entity(&entity_type);
774
775    let available_routes = routes
776        .iter()
777        .map(|r| RouteDescription {
778            path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
779            method: "GET".to_string(),
780            link_type: r.link_type.clone(),
781            direction: format!("{:?}", r.direction),
782            connected_to: r.connected_to.clone(),
783            description: r.description.clone(),
784        })
785        .collect();
786
787    Ok(Json(IntrospectionResponse {
788        entity_type,
789        entity_id,
790        available_routes,
791    }))
792}
793
794/// Handler générique pour GET sur chemins imbriqués illimités
795///
796/// Supporte des chemins comme:
797/// - GET /users/123/invoices/456/orders (liste les orders)
798/// - GET /users/123/invoices/456/orders/789 (get un order spécifique)
799pub async fn handle_nested_path_get(
800    State(state): State<AppState>,
801    Path(path): Path<String>,
802    Query(params): Query<QueryParams>,
803) -> Result<Json<serde_json::Value>, ExtractorError> {
804    // Parser le path en segments
805    let segments: Vec<String> = path
806        .trim_matches('/')
807        .split('/')
808        .map(|s| s.to_string())
809        .collect();
810
811    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
812    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
813    if segments.len() < 5 {
814        return Err(ExtractorError::InvalidPath);
815    }
816
817    // Utiliser l'extracteur récursif
818    let extractor =
819        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
820
821    // Si is_list, récupérer les liens depuis la dernière entité
822    if extractor.is_list {
823        // Valider toute la chaîne de liens avant de retourner les résultats
824        // Pour chaque segment avec un link_definition, vérifier que le lien existe
825        // SAUF pour le dernier segment si c'est une liste (ID = Uuid::nil())
826
827        use crate::links::registry::LinkDirection;
828
829        // VALIDATION COMPLÈTE DE LA CHAÎNE
830        for i in 0..extractor.chain.len() - 1 {
831            let current = &extractor.chain[i];
832            let next = &extractor.chain[i + 1];
833
834            // Si next.entity_id est Uuid::nil(), c'est une liste finale, on ne valide pas ce lien
835            if next.entity_id.is_nil() {
836                continue;
837            }
838
839            // Cas 1: Le segment a un link_definition → validation normale
840            if let Some(link_def) = &current.link_definition {
841                let link_exists = match current.link_direction {
842                    Some(LinkDirection::Forward) => {
843                        // Forward: current est la source, next est le target
844                        let links = state
845                            .link_service
846                            .find_by_source(
847                                &current.entity_id,
848                                Some(&link_def.link_type),
849                                Some(&link_def.target_type),
850                            )
851                            .await
852                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
853                        links.iter().any(|l| l.target_id == next.entity_id)
854                    }
855                    Some(LinkDirection::Reverse) => {
856                        // Reverse: current est le target, next est la source
857                        let links = state
858                            .link_service
859                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
860                            .await
861                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
862                        links.iter().any(|l| l.source_id == next.entity_id)
863                    }
864                    None => {
865                        return Err(ExtractorError::InvalidPath);
866                    }
867                };
868
869                if !link_exists {
870                    return Err(ExtractorError::LinkNotFound);
871                }
872            }
873            // Cas 2: Premier segment sans link_definition mais next a un link_definition
874            // → C'est le début d'une chaîne, on doit vérifier que current est lié à next
875            else if let Some(next_link_def) = &next.link_definition {
876                let link_exists = match next.link_direction {
877                    Some(LinkDirection::Forward) => {
878                        // Forward depuis current: current → next
879                        let links = state
880                            .link_service
881                            .find_by_source(
882                                &current.entity_id,
883                                Some(&next_link_def.link_type),
884                                Some(&next_link_def.target_type),
885                            )
886                            .await
887                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
888                        links.iter().any(|l| l.target_id == next.entity_id)
889                    }
890                    Some(LinkDirection::Reverse) => {
891                        // Reverse depuis current: current ← next (donc next est source)
892                        let links = state
893                            .link_service
894                            .find_by_target(
895                                &current.entity_id,
896                                None,
897                                Some(&next_link_def.link_type),
898                            )
899                            .await
900                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
901                        links.iter().any(|l| l.source_id == next.entity_id)
902                    }
903                    None => {
904                        return Err(ExtractorError::InvalidPath);
905                    }
906                };
907
908                if !link_exists {
909                    return Err(ExtractorError::LinkNotFound);
910                }
911            }
912        }
913
914        // Toute la chaîne est valide, récupérer les liens finaux
915        if let Some(link_def) = extractor.final_link_def() {
916            // Pour une liste, on veut l'ID du segment pénultième (celui qui a le lien)
917            let penultimate = extractor
918                .penultimate_segment()
919                .ok_or(ExtractorError::InvalidPath)?;
920            let entity_id = penultimate.entity_id;
921
922            use crate::links::registry::LinkDirection;
923
924            // Récupérer les liens selon la direction
925            let (links, enrichment_context) = match penultimate.link_direction {
926                Some(LinkDirection::Forward) => {
927                    // Forward: entity_id est la source
928                    let links = state
929                        .link_service
930                        .find_by_source(
931                            &entity_id,
932                            Some(&link_def.link_type),
933                            Some(&link_def.target_type),
934                        )
935                        .await
936                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
937                    (links, EnrichmentContext::FromSource)
938                }
939                Some(LinkDirection::Reverse) => {
940                    // Reverse: entity_id est le target, on cherche les sources
941                    let links = state
942                        .link_service
943                        .find_by_target(&entity_id, None, Some(&link_def.link_type))
944                        .await
945                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
946                    (links, EnrichmentContext::FromTarget)
947                }
948                None => {
949                    return Err(ExtractorError::InvalidPath);
950                }
951            };
952
953            // Enrichir TOUS les liens
954            let mut all_enriched =
955                enrich_links_with_entities(&state, links, enrichment_context, link_def).await?;
956
957            // Apply filters if provided
958            if let Some(filter_value) = params.filter_value() {
959                all_enriched = apply_link_filters(all_enriched, &filter_value);
960            }
961
962            let total = all_enriched.len();
963
964            // Apply pagination (ALWAYS paginate for nested links too)
965            let page = params.page();
966            let limit = params.limit();
967            let start = (page - 1) * limit;
968
969            let paginated_links: Vec<EnrichedLink> =
970                all_enriched.into_iter().skip(start).take(limit).collect();
971
972            Ok(Json(serde_json::json!({
973                "data": paginated_links,
974                "pagination": {
975                    "page": page,
976                    "limit": limit,
977                    "total": total,
978                    "total_pages": PaginationMeta::new(page, limit, total).total_pages,
979                    "has_next": PaginationMeta::new(page, limit, total).has_next,
980                    "has_prev": PaginationMeta::new(page, limit, total).has_prev
981                },
982                "link_type": link_def.link_type,
983                "direction": format!("{:?}", penultimate.link_direction),
984                "description": link_def.description
985            })))
986        } else {
987            Err(ExtractorError::InvalidPath)
988        }
989    } else {
990        // Item spécifique - récupérer le lien spécifique
991
992        use crate::links::registry::LinkDirection;
993
994        // VALIDATION COMPLÈTE DE LA CHAÎNE (aussi pour items spécifiques)
995        for i in 0..extractor.chain.len() - 1 {
996            let current = &extractor.chain[i];
997            let next = &extractor.chain[i + 1];
998
999            // Cas 1: Le segment a un link_definition → validation normale
1000            if let Some(link_def) = &current.link_definition {
1001                let link_exists = match current.link_direction {
1002                    Some(LinkDirection::Forward) => {
1003                        let links = state
1004                            .link_service
1005                            .find_by_source(
1006                                &current.entity_id,
1007                                Some(&link_def.link_type),
1008                                Some(&link_def.target_type),
1009                            )
1010                            .await
1011                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1012                        links.iter().any(|l| l.target_id == next.entity_id)
1013                    }
1014                    Some(LinkDirection::Reverse) => {
1015                        let links = state
1016                            .link_service
1017                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
1018                            .await
1019                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1020                        links.iter().any(|l| l.source_id == next.entity_id)
1021                    }
1022                    None => {
1023                        return Err(ExtractorError::InvalidPath);
1024                    }
1025                };
1026
1027                if !link_exists {
1028                    return Err(ExtractorError::LinkNotFound);
1029                }
1030            }
1031            // Cas 2: Premier segment sans link_definition mais next a un link_definition
1032            else if let Some(next_link_def) = &next.link_definition {
1033                let link_exists = match next.link_direction {
1034                    Some(LinkDirection::Forward) => {
1035                        let links = state
1036                            .link_service
1037                            .find_by_source(
1038                                &current.entity_id,
1039                                Some(&next_link_def.link_type),
1040                                Some(&next_link_def.target_type),
1041                            )
1042                            .await
1043                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1044                        links.iter().any(|l| l.target_id == next.entity_id)
1045                    }
1046                    Some(LinkDirection::Reverse) => {
1047                        let links = state
1048                            .link_service
1049                            .find_by_target(
1050                                &current.entity_id,
1051                                None,
1052                                Some(&next_link_def.link_type),
1053                            )
1054                            .await
1055                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1056                        links.iter().any(|l| l.source_id == next.entity_id)
1057                    }
1058                    None => {
1059                        return Err(ExtractorError::InvalidPath);
1060                    }
1061                };
1062
1063                if !link_exists {
1064                    return Err(ExtractorError::LinkNotFound);
1065                }
1066            }
1067        }
1068
1069        // Toute la chaîne est validée, récupérer le lien final
1070        if let Some(link_def) = extractor.final_link_def() {
1071            let (target_id, _) = extractor.final_target();
1072            let penultimate = extractor.penultimate_segment().unwrap();
1073
1074            // Récupérer le lien selon la direction
1075            let link = match penultimate.link_direction {
1076                Some(LinkDirection::Forward) => {
1077                    // Forward: penultimate est la source, target_id est le target
1078                    let links = state
1079                        .link_service
1080                        .find_by_source(
1081                            &penultimate.entity_id,
1082                            Some(&link_def.link_type),
1083                            Some(&link_def.target_type),
1084                        )
1085                        .await
1086                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1087
1088                    links
1089                        .into_iter()
1090                        .find(|l| l.target_id == target_id)
1091                        .ok_or(ExtractorError::LinkNotFound)?
1092                }
1093                Some(LinkDirection::Reverse) => {
1094                    // Reverse: penultimate est le target, target_id est la source
1095                    let links = state
1096                        .link_service
1097                        .find_by_target(&penultimate.entity_id, None, Some(&link_def.link_type))
1098                        .await
1099                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1100
1101                    links
1102                        .into_iter()
1103                        .find(|l| l.source_id == target_id)
1104                        .ok_or(ExtractorError::LinkNotFound)?
1105                }
1106                None => {
1107                    return Err(ExtractorError::InvalidPath);
1108                }
1109            };
1110
1111            // Enrichir le lien
1112            let enriched = enrich_links_with_entities(
1113                &state,
1114                vec![link],
1115                EnrichmentContext::DirectLink,
1116                link_def,
1117            )
1118            .await?;
1119
1120            Ok(Json(serde_json::json!({
1121                "link": enriched.first()
1122            })))
1123        } else {
1124            Err(ExtractorError::InvalidPath)
1125        }
1126    }
1127}
1128
1129/// Handler générique pour POST sur chemins imbriqués
1130///
1131/// Supporte des chemins comme:
1132/// - POST /users/123/invoices/456/orders (crée un nouvel order + link)
1133pub async fn handle_nested_path_post(
1134    State(state): State<AppState>,
1135    Path(path): Path<String>,
1136    Json(payload): Json<CreateLinkedEntityRequest>,
1137) -> Result<Response, ExtractorError> {
1138    let segments: Vec<String> = path
1139        .trim_matches('/')
1140        .split('/')
1141        .map(|s| s.to_string())
1142        .collect();
1143
1144    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
1145    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
1146    if segments.len() < 5 {
1147        return Err(ExtractorError::InvalidPath);
1148    }
1149
1150    let extractor =
1151        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
1152
1153    // Récupérer le dernier lien
1154    let link_def = extractor
1155        .final_link_def()
1156        .ok_or(ExtractorError::InvalidPath)?;
1157
1158    let (source_id, _) = extractor.final_target();
1159    let target_entity_type = &link_def.target_type;
1160
1161    // Récupérer le creator pour l'entité target
1162    let entity_creator = state
1163        .entity_creators
1164        .get(target_entity_type)
1165        .ok_or_else(|| {
1166            ExtractorError::JsonError(format!(
1167                "No entity creator registered for type: {}",
1168                target_entity_type
1169            ))
1170        })?;
1171
1172    // Créer la nouvelle entité
1173    let created_entity = entity_creator
1174        .create_from_json(payload.entity)
1175        .await
1176        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
1177
1178    // Extraire l'ID de l'entité créée
1179    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
1180        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
1181    })?;
1182    let target_entity_id = Uuid::parse_str(target_entity_id)
1183        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
1184
1185    // Créer le lien
1186    let link = LinkEntity::new(
1187        link_def.link_type.clone(),
1188        source_id,
1189        target_entity_id,
1190        payload.metadata,
1191    );
1192
1193    let created_link = state
1194        .link_service
1195        .create(link)
1196        .await
1197        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1198
1199    // Emit entity created event
1200    state.publish_event(FrameworkEvent::Entity(
1201        crate::core::events::EntityEvent::Created {
1202            entity_type: target_entity_type.clone(),
1203            entity_id: target_entity_id,
1204            data: created_entity.clone(),
1205        },
1206    ));
1207
1208    // Emit link created event
1209    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1210        link_type: created_link.link_type.clone(),
1211        link_id: created_link.id,
1212        source_id: created_link.source_id,
1213        target_id: created_link.target_id,
1214        metadata: created_link.metadata.clone(),
1215    }));
1216
1217    let response = serde_json::json!({
1218        "entity": created_entity,
1219        "link": created_link,
1220    });
1221
1222    Ok((StatusCode::CREATED, Json(response)).into_response())
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228    use crate::config::EntityConfig;
1229    use crate::core::LinkDefinition;
1230    use crate::storage::InMemoryLinkService;
1231
1232    fn create_test_state() -> AppState {
1233        let config = Arc::new(LinksConfig {
1234            entities: vec![
1235                EntityConfig {
1236                    singular: "user".to_string(),
1237                    plural: "users".to_string(),
1238                    auth: crate::config::EntityAuthConfig::default(),
1239                },
1240                EntityConfig {
1241                    singular: "car".to_string(),
1242                    plural: "cars".to_string(),
1243                    auth: crate::config::EntityAuthConfig::default(),
1244                },
1245            ],
1246            links: vec![LinkDefinition {
1247                link_type: "owner".to_string(),
1248                source_type: "user".to_string(),
1249                target_type: "car".to_string(),
1250                forward_route_name: "cars-owned".to_string(),
1251                reverse_route_name: "users-owners".to_string(),
1252                description: Some("User owns a car".to_string()),
1253                required_fields: None,
1254                auth: None,
1255            }],
1256            validation_rules: None,
1257        });
1258
1259        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1260        let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1261
1262        AppState {
1263            link_service,
1264            config,
1265            registry,
1266            entity_fetchers: Arc::new(HashMap::new()),
1267            entity_creators: Arc::new(HashMap::new()),
1268            event_bus: None,
1269        }
1270    }
1271
1272    #[test]
1273    fn test_state_creation() {
1274        let state = create_test_state();
1275        assert_eq!(state.config.entities.len(), 2);
1276        assert_eq!(state.config.links.len(), 1);
1277    }
1278}