data_modelling_core/models/
cross_domain.rs

1//! Cross-domain reference models
2//!
3//! Defines structures for referencing tables and relationships from other domains.
4//! This enables a domain to display and link to tables owned by other domains.
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// A reference to a table from another domain
10///
11/// This allows a domain to include tables from other domains in its canvas view.
12/// The referenced table is read-only in the importing domain - it can only be
13/// edited in its owning domain.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct CrossDomainTableRef {
16    /// Unique identifier for this reference
17    pub id: Uuid,
18
19    /// The domain that owns the table
20    pub source_domain: String,
21
22    /// The table ID in the source domain
23    pub table_id: Uuid,
24
25    /// Optional alias for display in this domain
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub display_alias: Option<String>,
28
29    /// Position override for this domain's canvas (if different from source)
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub position: Option<super::Position>,
32
33    /// Optional notes about why this table is referenced
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub notes: Option<String>,
36
37    /// When this reference was created
38    #[serde(default = "chrono::Utc::now")]
39    pub created_at: chrono::DateTime<chrono::Utc>,
40}
41
42impl CrossDomainTableRef {
43    /// Create a new cross-domain table reference
44    pub fn new(source_domain: String, table_id: Uuid) -> Self {
45        Self {
46            id: Uuid::new_v4(),
47            source_domain,
48            table_id,
49            display_alias: None,
50            position: None,
51            notes: None,
52            created_at: chrono::Utc::now(),
53        }
54    }
55}
56
57/// A reference to a relationship from another domain
58///
59/// When two tables from the same external domain are both imported,
60/// their original relationship can be shown as a read-only link.
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62pub struct CrossDomainRelationshipRef {
63    /// Unique identifier for this reference
64    pub id: Uuid,
65
66    /// The domain that owns the relationship
67    pub source_domain: String,
68
69    /// The relationship ID in the source domain
70    pub relationship_id: Uuid,
71
72    /// The source table ID (for quick lookup)
73    pub source_table_id: Uuid,
74
75    /// The target table ID (for quick lookup)
76    pub target_table_id: Uuid,
77
78    /// When this reference was created
79    #[serde(default = "chrono::Utc::now")]
80    pub created_at: chrono::DateTime<chrono::Utc>,
81}
82
83impl CrossDomainRelationshipRef {
84    /// Create a new cross-domain relationship reference
85    pub fn new(
86        source_domain: String,
87        relationship_id: Uuid,
88        source_table_id: Uuid,
89        target_table_id: Uuid,
90    ) -> Self {
91        Self {
92            id: Uuid::new_v4(),
93            source_domain,
94            relationship_id,
95            source_table_id,
96            target_table_id,
97            created_at: chrono::Utc::now(),
98        }
99    }
100}
101
102/// The cross-domain configuration for a domain
103///
104/// This is stored as `cross_domain.yaml` in each domain's directory.
105/// It defines which external tables and relationships should be visible
106/// in this domain's canvas view.
107#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
108pub struct CrossDomainConfig {
109    /// Schema version for forward compatibility
110    #[serde(default = "default_schema_version")]
111    pub schema_version: String,
112
113    /// Tables imported from other domains
114    #[serde(default)]
115    pub imported_tables: Vec<CrossDomainTableRef>,
116
117    /// Relationships imported from other domains (read-only display)
118    /// These are automatically populated when both ends of a relationship
119    /// from another domain are imported.
120    #[serde(default)]
121    pub imported_relationships: Vec<CrossDomainRelationshipRef>,
122}
123
124fn default_schema_version() -> String {
125    "1.0".to_string()
126}
127
128impl CrossDomainConfig {
129    /// Create a new empty cross-domain configuration
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Add a table reference from another domain
135    /// Returns the index of the added or existing reference
136    pub fn add_table_ref(&mut self, source_domain: String, table_id: Uuid) -> usize {
137        // Check if already exists
138        if let Some(idx) = self
139            .imported_tables
140            .iter()
141            .position(|t| t.source_domain == source_domain && t.table_id == table_id)
142        {
143            return idx;
144        }
145
146        let ref_entry = CrossDomainTableRef::new(source_domain, table_id);
147        self.imported_tables.push(ref_entry);
148        self.imported_tables.len() - 1
149    }
150
151    /// Get a table reference by index
152    pub fn get_table_ref(&self, idx: usize) -> Option<&CrossDomainTableRef> {
153        self.imported_tables.get(idx)
154    }
155
156    /// Remove a table reference
157    pub fn remove_table_ref(&mut self, table_id: Uuid) -> bool {
158        let initial_len = self.imported_tables.len();
159        self.imported_tables.retain(|t| t.table_id != table_id);
160
161        // Also remove any relationship refs that involve this table
162        self.imported_relationships
163            .retain(|r| r.source_table_id != table_id && r.target_table_id != table_id);
164
165        self.imported_tables.len() != initial_len
166    }
167
168    /// Add a relationship reference (for read-only display)
169    /// Returns the index of the added or existing reference
170    pub fn add_relationship_ref(
171        &mut self,
172        source_domain: String,
173        relationship_id: Uuid,
174        source_table_id: Uuid,
175        target_table_id: Uuid,
176    ) -> usize {
177        // Check if already exists
178        if let Some(idx) = self
179            .imported_relationships
180            .iter()
181            .position(|r| r.source_domain == source_domain && r.relationship_id == relationship_id)
182        {
183            return idx;
184        }
185
186        let ref_entry = CrossDomainRelationshipRef::new(
187            source_domain,
188            relationship_id,
189            source_table_id,
190            target_table_id,
191        );
192        self.imported_relationships.push(ref_entry);
193        self.imported_relationships.len() - 1
194    }
195
196    /// Get a relationship reference by index
197    pub fn get_relationship_ref(&self, idx: usize) -> Option<&CrossDomainRelationshipRef> {
198        self.imported_relationships.get(idx)
199    }
200
201    /// Remove a relationship reference
202    pub fn remove_relationship_ref(&mut self, relationship_id: Uuid) -> bool {
203        let initial_len = self.imported_relationships.len();
204        self.imported_relationships
205            .retain(|r| r.relationship_id != relationship_id);
206        self.imported_relationships.len() != initial_len
207    }
208
209    /// Get all imported table IDs from a specific domain
210    pub fn get_tables_from_domain(&self, domain: &str) -> Vec<Uuid> {
211        self.imported_tables
212            .iter()
213            .filter(|t| t.source_domain == domain)
214            .map(|t| t.table_id)
215            .collect()
216    }
217
218    /// Check if a table is imported from another domain
219    pub fn is_table_imported(&self, table_id: Uuid) -> bool {
220        self.imported_tables.iter().any(|t| t.table_id == table_id)
221    }
222
223    /// Get the source domain for an imported table
224    pub fn get_table_source_domain(&self, table_id: Uuid) -> Option<&str> {
225        self.imported_tables
226            .iter()
227            .find(|t| t.table_id == table_id)
228            .map(|t| t.source_domain.as_str())
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_add_table_ref() {
238        let mut config = CrossDomainConfig::new();
239        let table_id = Uuid::new_v4();
240
241        config.add_table_ref("finance".to_string(), table_id);
242
243        assert_eq!(config.imported_tables.len(), 1);
244        assert_eq!(config.imported_tables[0].source_domain, "finance");
245        assert_eq!(config.imported_tables[0].table_id, table_id);
246    }
247
248    #[test]
249    fn test_duplicate_table_ref() {
250        let mut config = CrossDomainConfig::new();
251        let table_id = Uuid::new_v4();
252
253        config.add_table_ref("finance".to_string(), table_id);
254        config.add_table_ref("finance".to_string(), table_id);
255
256        // Should not add duplicate
257        assert_eq!(config.imported_tables.len(), 1);
258    }
259
260    #[test]
261    fn test_remove_table_ref_removes_relationships() {
262        let mut config = CrossDomainConfig::new();
263        let table_a = Uuid::new_v4();
264        let table_b = Uuid::new_v4();
265        let rel_id = Uuid::new_v4();
266
267        config.add_table_ref("finance".to_string(), table_a);
268        config.add_table_ref("finance".to_string(), table_b);
269        config.add_relationship_ref("finance".to_string(), rel_id, table_a, table_b);
270
271        assert_eq!(config.imported_relationships.len(), 1);
272
273        config.remove_table_ref(table_a);
274
275        // Relationship should also be removed
276        assert_eq!(config.imported_relationships.len(), 0);
277    }
278}