use super::enums::{
Cardinality, EndpointCardinality, FlowDirection, InfrastructureType, RelationshipType,
};
use super::table::{ContactDetails, SlaProperty};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ForeignKeyDetails {
#[serde(alias = "source_column")]
pub source_column: String,
#[serde(alias = "target_column")]
pub target_column: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ETLJobMetadata {
#[serde(alias = "job_name")]
pub job_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ConnectionPoint {
pub x: f64,
pub y: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct VisualMetadata {
#[serde(
skip_serializing_if = "Option::is_none",
alias = "source_connection_point"
)]
pub source_connection_point: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
alias = "target_connection_point"
)]
pub target_connection_point: Option<String>,
#[serde(default, alias = "routing_waypoints")]
pub routing_waypoints: Vec<ConnectionPoint>,
#[serde(skip_serializing_if = "Option::is_none", alias = "label_position")]
pub label_position: Option<ConnectionPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum ConnectionHandle {
TopLeft,
TopCenter,
TopRight,
RightTop,
RightCenter,
RightBottom,
BottomRight,
BottomCenter,
BottomLeft,
LeftBottom,
LeftCenter,
LeftTop,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Relationship {
pub id: Uuid,
#[serde(alias = "source_table_id")]
pub source_table_id: Uuid,
#[serde(alias = "target_table_id")]
pub target_table_id: Uuid,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "source_key")]
pub source_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "target_key")]
pub target_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cardinality: Option<Cardinality>,
#[serde(skip_serializing_if = "Option::is_none", alias = "source_optional")]
pub source_optional: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", alias = "target_optional")]
pub target_optional: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", alias = "source_cardinality")]
pub source_cardinality: Option<EndpointCardinality>,
#[serde(skip_serializing_if = "Option::is_none", alias = "target_cardinality")]
pub target_cardinality: Option<EndpointCardinality>,
#[serde(skip_serializing_if = "Option::is_none", alias = "flow_direction")]
pub flow_direction: Option<FlowDirection>,
#[serde(skip_serializing_if = "Option::is_none", alias = "foreign_key_details")]
pub foreign_key_details: Option<ForeignKeyDetails>,
#[serde(skip_serializing_if = "Option::is_none", alias = "etl_job_metadata")]
pub etl_job_metadata: Option<ETLJobMetadata>,
#[serde(skip_serializing_if = "Option::is_none", alias = "relationship_type")]
pub relationship_type: Option<RelationshipType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sla: Option<Vec<SlaProperty>>,
#[serde(skip_serializing_if = "Option::is_none", alias = "contact_details")]
pub contact_details: Option<ContactDetails>,
#[serde(skip_serializing_if = "Option::is_none", alias = "infrastructure_type")]
pub infrastructure_type: Option<InfrastructureType>,
#[serde(skip_serializing_if = "Option::is_none", alias = "visual_metadata")]
pub visual_metadata: Option<VisualMetadata>,
#[serde(skip_serializing_if = "Option::is_none", alias = "drawio_edge_id")]
pub drawio_edge_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", alias = "source_handle")]
pub source_handle: Option<ConnectionHandle>,
#[serde(skip_serializing_if = "Option::is_none", alias = "target_handle")]
pub target_handle: Option<ConnectionHandle>,
#[serde(alias = "created_at")]
pub created_at: DateTime<Utc>,
#[serde(alias = "updated_at")]
pub updated_at: DateTime<Utc>,
}
impl Relationship {
pub fn new(source_table_id: Uuid, target_table_id: Uuid) -> Self {
let now = Utc::now();
let id = Self::generate_id(source_table_id, target_table_id);
Self {
id,
source_table_id,
target_table_id,
label: None,
source_key: None,
target_key: None,
cardinality: None,
source_optional: None,
target_optional: None,
source_cardinality: None,
target_cardinality: None,
flow_direction: None,
foreign_key_details: None,
etl_job_metadata: None,
relationship_type: None,
notes: None,
owner: None,
sla: None,
contact_details: None,
infrastructure_type: None,
visual_metadata: None,
drawio_edge_id: None,
color: None,
source_handle: None,
target_handle: None,
created_at: now,
updated_at: now,
}
}
pub fn generate_id(_source_table_id: Uuid, _target_table_id: Uuid) -> Uuid {
Uuid::new_v4()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relationship_new() {
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let rel = Relationship::new(source_id, target_id);
assert_eq!(rel.source_table_id, source_id);
assert_eq!(rel.target_table_id, target_id);
assert!(rel.label.is_none());
assert!(rel.source_key.is_none());
assert!(rel.target_key.is_none());
}
#[test]
fn test_relationship_with_label_and_keys() {
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let mut rel = Relationship::new(source_id, target_id);
rel.label = Some("references".to_string());
rel.source_key = Some("customer_id".to_string());
rel.target_key = Some("id".to_string());
let json = serde_json::to_string(&rel).unwrap();
let parsed: Relationship = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.label, Some("references".to_string()));
assert_eq!(parsed.source_key, Some("customer_id".to_string()));
assert_eq!(parsed.target_key, Some("id".to_string()));
}
#[test]
fn test_relationship_yaml_roundtrip() {
let source_id = Uuid::new_v4();
let target_id = Uuid::new_v4();
let mut rel = Relationship::new(source_id, target_id);
rel.label = Some("has many".to_string());
rel.source_key = Some("order_id".to_string());
rel.target_key = Some("id".to_string());
rel.source_cardinality = Some(EndpointCardinality::ExactlyOne);
rel.target_cardinality = Some(EndpointCardinality::ZeroOrMany);
let yaml = serde_yaml::to_string(&rel).unwrap();
let parsed: Relationship = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(parsed.label, Some("has many".to_string()));
assert_eq!(parsed.source_key, Some("order_id".to_string()));
assert_eq!(parsed.target_key, Some("id".to_string()));
assert_eq!(
parsed.source_cardinality,
Some(EndpointCardinality::ExactlyOne)
);
assert_eq!(
parsed.target_cardinality,
Some(EndpointCardinality::ZeroOrMany)
);
}
#[test]
fn test_relationship_backward_compatibility() {
let yaml = r#"
id: 550e8400-e29b-41d4-a716-446655440000
sourceTableId: 660e8400-e29b-41d4-a716-446655440001
targetTableId: 770e8400-e29b-41d4-a716-446655440002
createdAt: 2025-01-01T09:00:00Z
updatedAt: 2025-01-01T09:00:00Z
"#;
let parsed: Relationship = serde_yaml::from_str(yaml).unwrap();
assert!(parsed.label.is_none());
assert!(parsed.source_key.is_none());
assert!(parsed.target_key.is_none());
}
}