data_modelling_sdk/models/
relationship.rs

1//! Relationship model for the SDK
2
3use super::enums::{
4    Cardinality, EndpointCardinality, FlowDirection, InfrastructureType, RelationshipType,
5};
6use super::table::{ContactDetails, SlaProperty};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11/// Foreign key column mapping details
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct ForeignKeyDetails {
15    /// Column name in the source table
16    pub source_column: String,
17    /// Column name in the target table
18    pub target_column: String,
19}
20
21/// ETL job metadata for data flow relationships
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(rename_all = "camelCase")]
24pub struct ETLJobMetadata {
25    /// Name of the ETL job that creates this relationship
26    pub job_name: String,
27    /// Optional notes about the ETL job
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub notes: Option<String>,
30    /// Job execution frequency (e.g., "daily", "hourly")
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub frequency: Option<String>,
33}
34
35/// Connection point coordinates for relationship visualization
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub struct ConnectionPoint {
38    /// X coordinate
39    pub x: f64,
40    /// Y coordinate
41    pub y: f64,
42}
43
44/// Visual metadata for relationship rendering on canvas
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46#[serde(rename_all = "camelCase")]
47pub struct VisualMetadata {
48    /// Connection point identifier on source table
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub source_connection_point: Option<String>,
51    /// Connection point identifier on target table
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub target_connection_point: Option<String>,
54    /// Waypoints for routing the relationship line
55    #[serde(default)]
56    pub routing_waypoints: Vec<ConnectionPoint>,
57    /// Position for the relationship label
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub label_position: Option<ConnectionPoint>,
60}
61
62/// Edge attachment point positions on a node
63///
64/// Defines 12 possible handle positions around the perimeter of a node,
65/// organized by edge (top, right, bottom, left) and position on that edge.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67#[serde(rename_all = "kebab-case")]
68pub enum ConnectionHandle {
69    /// Top edge, left position
70    TopLeft,
71    /// Top edge, center position
72    TopCenter,
73    /// Top edge, right position
74    TopRight,
75    /// Right edge, top position
76    RightTop,
77    /// Right edge, center position
78    RightCenter,
79    /// Right edge, bottom position
80    RightBottom,
81    /// Bottom edge, right position
82    BottomRight,
83    /// Bottom edge, center position
84    BottomCenter,
85    /// Bottom edge, left position
86    BottomLeft,
87    /// Left edge, bottom position
88    LeftBottom,
89    /// Left edge, center position
90    LeftCenter,
91    /// Left edge, top position
92    LeftTop,
93}
94
95/// Relationship model representing a connection between two tables
96///
97/// Relationships can represent foreign keys, data flows, dependencies, or ETL transformations.
98/// They connect a source table to a target table with optional metadata about cardinality,
99/// foreign key details, and ETL job information.
100///
101/// # Example
102///
103/// ```rust
104/// use data_modelling_sdk::models::Relationship;
105///
106/// let source_id = uuid::Uuid::new_v4();
107/// let target_id = uuid::Uuid::new_v4();
108/// let relationship = Relationship::new(source_id, target_id);
109/// ```
110///
111/// # Example with Metadata (Data Flow Relationship)
112///
113/// ```rust
114/// use data_modelling_sdk::models::{Relationship, InfrastructureType, ContactDetails, SlaProperty};
115/// use serde_json::json;
116/// use uuid::Uuid;
117///
118/// let source_id = Uuid::new_v4();
119/// let target_id = Uuid::new_v4();
120/// let mut relationship = Relationship::new(source_id, target_id);
121/// relationship.owner = Some("Data Engineering Team".to_string());
122/// relationship.infrastructure_type = Some(InfrastructureType::Kafka);
123/// relationship.contact_details = Some(ContactDetails {
124///     email: Some("team@example.com".to_string()),
125///     phone: None,
126///     name: Some("Data Team".to_string()),
127///     role: Some("Data Owner".to_string()),
128///     other: None,
129/// });
130/// relationship.sla = Some(vec![SlaProperty {
131///     property: "latency".to_string(),
132///     value: json!(2),
133///     unit: "hours".to_string(),
134///     description: Some("Data flow must complete within 2 hours".to_string()),
135///     element: None,
136///     driver: Some("operational".to_string()),
137///     scheduler: None,
138///     schedule: None,
139/// }]);
140/// relationship.notes = Some("ETL pipeline from source to target".to_string());
141/// ```
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143#[serde(rename_all = "camelCase")]
144pub struct Relationship {
145    /// Unique identifier for the relationship (UUIDv4)
146    pub id: Uuid,
147    /// ID of the source table
148    pub source_table_id: Uuid,
149    /// ID of the target table
150    pub target_table_id: Uuid,
151    /// Legacy cardinality (OneToOne, OneToMany, ManyToMany) - for backward compatibility
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub cardinality: Option<Cardinality>,
154    /// Whether the source side is optional (nullable foreign key) - legacy field
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub source_optional: Option<bool>,
157    /// Whether the target side is optional - legacy field
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub target_optional: Option<bool>,
160    /// Crow's feet cardinality at the source end (zeroOrOne, exactlyOne, zeroOrMany, oneOrMany)
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub source_cardinality: Option<EndpointCardinality>,
163    /// Crow's feet cardinality at the target end (zeroOrOne, exactlyOne, zeroOrMany, oneOrMany)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub target_cardinality: Option<EndpointCardinality>,
166    /// Direction of data flow (sourceToTarget, targetToSource, bidirectional)
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub flow_direction: Option<FlowDirection>,
169    /// Foreign key column mapping details
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub foreign_key_details: Option<ForeignKeyDetails>,
172    /// ETL job metadata for data flow relationships
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub etl_job_metadata: Option<ETLJobMetadata>,
175    /// Type of relationship (ForeignKey, DataFlow, Dependency, ETL)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub relationship_type: Option<RelationshipType>,
178    /// Optional notes about the relationship
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub notes: Option<String>,
181    /// Owner information (person, team, or organization name) for Data Flow relationships
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub owner: Option<String>,
184    /// SLA (Service Level Agreement) information (ODCS-inspired but lightweight format)
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub sla: Option<Vec<SlaProperty>>,
187    /// Contact details for responsible parties
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub contact_details: Option<ContactDetails>,
190    /// Infrastructure type (hosting platform, service, or tool) for Data Flow relationships
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub infrastructure_type: Option<InfrastructureType>,
193    /// Visual metadata for canvas rendering
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub visual_metadata: Option<VisualMetadata>,
196    /// Draw.io edge ID for diagram integration
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub drawio_edge_id: Option<String>,
199    /// Color for the relationship line in the UI (hex color code or named color)
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub color: Option<String>,
202    /// Edge attachment point on the source node
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub source_handle: Option<ConnectionHandle>,
205    /// Edge attachment point on the target node
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub target_handle: Option<ConnectionHandle>,
208    /// Creation timestamp
209    pub created_at: DateTime<Utc>,
210    /// Last update timestamp
211    pub updated_at: DateTime<Utc>,
212}
213
214impl Relationship {
215    /// Create a new relationship between two tables
216    ///
217    /// # Arguments
218    ///
219    /// * `source_table_id` - UUID of the source table
220    /// * `target_table_id` - UUID of the target table
221    ///
222    /// # Returns
223    ///
224    /// A new `Relationship` instance with a generated UUIDv4 ID and current timestamps.
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// use data_modelling_sdk::models::Relationship;
230    ///
231    /// let source_id = uuid::Uuid::new_v4();
232    /// let target_id = uuid::Uuid::new_v4();
233    /// let rel = Relationship::new(source_id, target_id);
234    /// ```
235    pub fn new(source_table_id: Uuid, target_table_id: Uuid) -> Self {
236        let now = Utc::now();
237        let id = Self::generate_id(source_table_id, target_table_id);
238        Self {
239            id,
240            source_table_id,
241            target_table_id,
242            cardinality: None,
243            source_optional: None,
244            target_optional: None,
245            source_cardinality: None,
246            target_cardinality: None,
247            flow_direction: None,
248            foreign_key_details: None,
249            etl_job_metadata: None,
250            relationship_type: None,
251            notes: None,
252            owner: None,
253            sla: None,
254            contact_details: None,
255            infrastructure_type: None,
256            visual_metadata: None,
257            drawio_edge_id: None,
258            color: None,
259            source_handle: None,
260            target_handle: None,
261            created_at: now,
262            updated_at: now,
263        }
264    }
265
266    /// Generate a UUIDv4 for a new relationship id.
267    ///
268    /// Note: params are retained for backward-compatibility with previous deterministic-v5 API.
269    pub fn generate_id(_source_table_id: Uuid, _target_table_id: Uuid) -> Uuid {
270        Uuid::new_v4()
271    }
272}