data_modelling_core/models/
relationship.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct ForeignKeyDetails {
15 #[serde(alias = "source_column")]
17 pub source_column: String,
18 #[serde(alias = "target_column")]
20 pub target_column: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(rename_all = "camelCase")]
26pub struct ETLJobMetadata {
27 #[serde(alias = "job_name")]
29 pub job_name: String,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub notes: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub frequency: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
40pub struct ConnectionPoint {
41 pub x: f64,
43 pub y: f64,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(rename_all = "camelCase")]
50pub struct VisualMetadata {
51 #[serde(
53 skip_serializing_if = "Option::is_none",
54 alias = "source_connection_point"
55 )]
56 pub source_connection_point: Option<String>,
57 #[serde(
59 skip_serializing_if = "Option::is_none",
60 alias = "target_connection_point"
61 )]
62 pub target_connection_point: Option<String>,
63 #[serde(default, alias = "routing_waypoints")]
65 pub routing_waypoints: Vec<ConnectionPoint>,
66 #[serde(skip_serializing_if = "Option::is_none", alias = "label_position")]
68 pub label_position: Option<ConnectionPoint>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76#[serde(rename_all = "kebab-case")]
77pub enum ConnectionHandle {
78 TopLeft,
80 TopCenter,
82 TopRight,
84 RightTop,
86 RightCenter,
88 RightBottom,
90 BottomRight,
92 BottomCenter,
94 BottomLeft,
96 LeftBottom,
98 LeftCenter,
100 LeftTop,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "camelCase")]
153pub struct Relationship {
154 pub id: Uuid,
156 #[serde(alias = "source_table_id")]
158 pub source_table_id: Uuid,
159 #[serde(alias = "target_table_id")]
161 pub target_table_id: Uuid,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub label: Option<String>,
165 #[serde(skip_serializing_if = "Option::is_none", alias = "source_key")]
167 pub source_key: Option<String>,
168 #[serde(skip_serializing_if = "Option::is_none", alias = "target_key")]
170 pub target_key: Option<String>,
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub cardinality: Option<Cardinality>,
174 #[serde(skip_serializing_if = "Option::is_none", alias = "source_optional")]
176 pub source_optional: Option<bool>,
177 #[serde(skip_serializing_if = "Option::is_none", alias = "target_optional")]
179 pub target_optional: Option<bool>,
180 #[serde(skip_serializing_if = "Option::is_none", alias = "source_cardinality")]
182 pub source_cardinality: Option<EndpointCardinality>,
183 #[serde(skip_serializing_if = "Option::is_none", alias = "target_cardinality")]
185 pub target_cardinality: Option<EndpointCardinality>,
186 #[serde(skip_serializing_if = "Option::is_none", alias = "flow_direction")]
188 pub flow_direction: Option<FlowDirection>,
189 #[serde(skip_serializing_if = "Option::is_none", alias = "foreign_key_details")]
191 pub foreign_key_details: Option<ForeignKeyDetails>,
192 #[serde(skip_serializing_if = "Option::is_none", alias = "etl_job_metadata")]
194 pub etl_job_metadata: Option<ETLJobMetadata>,
195 #[serde(skip_serializing_if = "Option::is_none", alias = "relationship_type")]
197 pub relationship_type: Option<RelationshipType>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub notes: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub owner: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
206 pub sla: Option<Vec<SlaProperty>>,
207 #[serde(skip_serializing_if = "Option::is_none", alias = "contact_details")]
209 pub contact_details: Option<ContactDetails>,
210 #[serde(skip_serializing_if = "Option::is_none", alias = "infrastructure_type")]
212 pub infrastructure_type: Option<InfrastructureType>,
213 #[serde(skip_serializing_if = "Option::is_none", alias = "visual_metadata")]
215 pub visual_metadata: Option<VisualMetadata>,
216 #[serde(skip_serializing_if = "Option::is_none", alias = "drawio_edge_id")]
218 pub drawio_edge_id: Option<String>,
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub color: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none", alias = "source_handle")]
224 pub source_handle: Option<ConnectionHandle>,
225 #[serde(skip_serializing_if = "Option::is_none", alias = "target_handle")]
227 pub target_handle: Option<ConnectionHandle>,
228 #[serde(alias = "created_at")]
230 pub created_at: DateTime<Utc>,
231 #[serde(alias = "updated_at")]
233 pub updated_at: DateTime<Utc>,
234}
235
236impl Relationship {
237 pub fn new(source_table_id: Uuid, target_table_id: Uuid) -> Self {
258 let now = Utc::now();
259 let id = Self::generate_id(source_table_id, target_table_id);
260 Self {
261 id,
262 source_table_id,
263 target_table_id,
264 label: None,
265 source_key: None,
266 target_key: None,
267 cardinality: None,
268 source_optional: None,
269 target_optional: None,
270 source_cardinality: None,
271 target_cardinality: None,
272 flow_direction: None,
273 foreign_key_details: None,
274 etl_job_metadata: None,
275 relationship_type: None,
276 notes: None,
277 owner: None,
278 sla: None,
279 contact_details: None,
280 infrastructure_type: None,
281 visual_metadata: None,
282 drawio_edge_id: None,
283 color: None,
284 source_handle: None,
285 target_handle: None,
286 created_at: now,
287 updated_at: now,
288 }
289 }
290
291 pub fn generate_id(_source_table_id: Uuid, _target_table_id: Uuid) -> Uuid {
295 Uuid::new_v4()
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn test_relationship_new() {
305 let source_id = Uuid::new_v4();
306 let target_id = Uuid::new_v4();
307 let rel = Relationship::new(source_id, target_id);
308
309 assert_eq!(rel.source_table_id, source_id);
310 assert_eq!(rel.target_table_id, target_id);
311 assert!(rel.label.is_none());
312 assert!(rel.source_key.is_none());
313 assert!(rel.target_key.is_none());
314 }
315
316 #[test]
317 fn test_relationship_with_label_and_keys() {
318 let source_id = Uuid::new_v4();
319 let target_id = Uuid::new_v4();
320 let mut rel = Relationship::new(source_id, target_id);
321 rel.label = Some("references".to_string());
322 rel.source_key = Some("customer_id".to_string());
323 rel.target_key = Some("id".to_string());
324
325 let json = serde_json::to_string(&rel).unwrap();
326 let parsed: Relationship = serde_json::from_str(&json).unwrap();
327
328 assert_eq!(parsed.label, Some("references".to_string()));
329 assert_eq!(parsed.source_key, Some("customer_id".to_string()));
330 assert_eq!(parsed.target_key, Some("id".to_string()));
331 }
332
333 #[test]
334 fn test_relationship_yaml_roundtrip() {
335 let source_id = Uuid::new_v4();
336 let target_id = Uuid::new_v4();
337 let mut rel = Relationship::new(source_id, target_id);
338 rel.label = Some("has many".to_string());
339 rel.source_key = Some("order_id".to_string());
340 rel.target_key = Some("id".to_string());
341 rel.source_cardinality = Some(EndpointCardinality::ExactlyOne);
342 rel.target_cardinality = Some(EndpointCardinality::ZeroOrMany);
343
344 let yaml = serde_yaml::to_string(&rel).unwrap();
345 let parsed: Relationship = serde_yaml::from_str(&yaml).unwrap();
346
347 assert_eq!(parsed.label, Some("has many".to_string()));
348 assert_eq!(parsed.source_key, Some("order_id".to_string()));
349 assert_eq!(parsed.target_key, Some("id".to_string()));
350 assert_eq!(
351 parsed.source_cardinality,
352 Some(EndpointCardinality::ExactlyOne)
353 );
354 assert_eq!(
355 parsed.target_cardinality,
356 Some(EndpointCardinality::ZeroOrMany)
357 );
358 }
359
360 #[test]
361 fn test_relationship_backward_compatibility() {
362 let yaml = r#"
364id: 550e8400-e29b-41d4-a716-446655440000
365sourceTableId: 660e8400-e29b-41d4-a716-446655440001
366targetTableId: 770e8400-e29b-41d4-a716-446655440002
367createdAt: 2025-01-01T09:00:00Z
368updatedAt: 2025-01-01T09:00:00Z
369"#;
370 let parsed: Relationship = serde_yaml::from_str(yaml).unwrap();
371 assert!(parsed.label.is_none());
372 assert!(parsed.source_key.is_none());
373 assert!(parsed.target_key.is_none());
374 }
375}