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}