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