data_modelling_sdk/
lib.rs

1//! Data Modelling SDK - Shared library for model operations across platforms
2//!
3//! Provides unified interfaces for:
4//! - File/folder operations (via storage backends)
5//! - Model loading/saving
6//! - Import/export functionality
7//! - Validation logic
8//! - Authentication types (shared across web, desktop, mobile)
9//! - Workspace management types
10
11pub mod auth;
12pub mod export;
13#[cfg(feature = "git")]
14pub mod git;
15pub mod import;
16pub mod model;
17pub mod models;
18pub mod storage;
19pub mod validation;
20pub mod workspace;
21
22// Re-export commonly used types
23#[cfg(feature = "api-backend")]
24pub use storage::api::ApiStorageBackend;
25#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
26pub use storage::browser::BrowserStorageBackend;
27#[cfg(feature = "native-fs")]
28pub use storage::filesystem::FileSystemStorageBackend;
29pub use storage::{StorageBackend, StorageError};
30
31#[cfg(feature = "png-export")]
32pub use export::PNGExporter;
33pub use export::{
34    AvroExporter, ExportError, ExportResult, JSONSchemaExporter, ODCSExporter, ProtobufExporter,
35    SQLExporter,
36};
37pub use import::{
38    AvroImporter, ImportError, ImportResult, JSONSchemaImporter, ODCSImporter, ProtobufImporter,
39    SQLImporter,
40};
41#[cfg(feature = "api-backend")]
42pub use model::ApiModelLoader;
43pub use model::{ModelLoader, ModelSaver};
44pub use validation::{
45    RelationshipValidationError, RelationshipValidationResult, TableValidationError,
46    TableValidationResult,
47};
48
49// Re-export models
50pub use models::enums::*;
51pub use models::{Column, DataModel, ForeignKey, Relationship, Table};
52
53// Re-export auth types
54pub use auth::{
55    AuthMode, AuthState, GitHubEmail, InitiateOAuthRequest, InitiateOAuthResponse,
56    SelectEmailRequest,
57};
58
59// Re-export workspace types
60pub use workspace::{
61    CreateWorkspaceRequest, CreateWorkspaceResponse, ListProfilesResponse, LoadProfileRequest,
62    ProfileInfo, WorkspaceInfo,
63};
64
65// Re-export Git types
66#[cfg(feature = "git")]
67pub use git::{GitError, GitService, GitStatus};
68
69// WASM bindings for import/export functions
70#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
71mod wasm {
72    use crate::export::ExportError;
73    use crate::import::{ImportError, ImportResult};
74    use crate::models::DataModel;
75    use js_sys;
76    use serde_json;
77    use serde_yaml;
78    use uuid;
79    use wasm_bindgen::prelude::*;
80    use wasm_bindgen_futures;
81
82    /// Convert ImportError to JsValue for JavaScript error handling
83    fn import_error_to_js(err: ImportError) -> JsValue {
84        JsValue::from_str(&err.to_string())
85    }
86
87    /// Convert ExportError to JsValue for JavaScript error handling
88    fn export_error_to_js(err: ExportError) -> JsValue {
89        JsValue::from_str(&err.to_string())
90    }
91
92    /// Serialize ImportResult to JSON string
93    fn serialize_import_result(result: &ImportResult) -> Result<String, JsValue> {
94        serde_json::to_string(result)
95            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
96    }
97
98    /// Deserialize workspace structure from JSON string
99    fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
100        serde_json::from_str(json)
101            .map_err(|e| JsValue::from_str(&format!("Deserialization error: {}", e)))
102    }
103
104    /// Parse ODCS YAML content and return a structured workspace representation.
105    ///
106    /// # Arguments
107    ///
108    /// * `yaml_content` - ODCS YAML content as a string
109    ///
110    /// # Returns
111    ///
112    /// JSON string containing ImportResult object, or JsValue error
113    #[wasm_bindgen]
114    pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
115        let mut importer = crate::import::ODCSImporter::new();
116        match importer.import(yaml_content) {
117            Ok(result) => serialize_import_result(&result),
118            Err(err) => Err(import_error_to_js(err)),
119        }
120    }
121
122    /// Export a workspace structure to ODCS YAML format.
123    ///
124    /// # Arguments
125    ///
126    /// * `workspace_json` - JSON string containing workspace/data model structure
127    ///
128    /// # Returns
129    ///
130    /// ODCS YAML format string, or JsValue error
131    #[wasm_bindgen]
132    pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
133        let model = deserialize_workspace(workspace_json)?;
134
135        // Export all tables as separate YAML documents, joined with ---\n
136        let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
137
138        // Combine all YAML documents into a single multi-document string
139        let yaml_docs: Vec<String> = exports.values().cloned().collect();
140        Ok(yaml_docs.join("\n---\n"))
141    }
142
143    /// Import data model from SQL CREATE TABLE statements.
144    ///
145    /// # Arguments
146    ///
147    /// * `sql_content` - SQL CREATE TABLE statements
148    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", "databricks")
149    ///
150    /// # Returns
151    ///
152    /// JSON string containing ImportResult object, or JsValue error
153    #[wasm_bindgen]
154    pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
155        let importer = crate::import::SQLImporter::new(dialect);
156        match importer.parse(sql_content) {
157            Ok(result) => serialize_import_result(&result),
158            Err(err) => Err(JsValue::from_str(&format!("Parse error: {}", err))),
159        }
160    }
161
162    /// Import data model from AVRO schema.
163    ///
164    /// # Arguments
165    ///
166    /// * `avro_content` - AVRO schema JSON as a string
167    ///
168    /// # Returns
169    ///
170    /// JSON string containing ImportResult object, or JsValue error
171    #[wasm_bindgen]
172    pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
173        let importer = crate::import::AvroImporter::new();
174        match importer.import(avro_content) {
175            Ok(result) => serialize_import_result(&result),
176            Err(err) => Err(import_error_to_js(err)),
177        }
178    }
179
180    /// Import data model from JSON Schema definition.
181    ///
182    /// # Arguments
183    ///
184    /// * `json_schema_content` - JSON Schema definition as a string
185    ///
186    /// # Returns
187    ///
188    /// JSON string containing ImportResult object, or JsValue error
189    #[wasm_bindgen]
190    pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
191        let importer = crate::import::JSONSchemaImporter::new();
192        match importer.import(json_schema_content) {
193            Ok(result) => serialize_import_result(&result),
194            Err(err) => Err(import_error_to_js(err)),
195        }
196    }
197
198    /// Import data model from Protobuf schema.
199    ///
200    /// # Arguments
201    ///
202    /// * `protobuf_content` - Protobuf schema text
203    ///
204    /// # Returns
205    ///
206    /// JSON string containing ImportResult object, or JsValue error
207    #[wasm_bindgen]
208    pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
209        let importer = crate::import::ProtobufImporter::new();
210        match importer.import(protobuf_content) {
211            Ok(result) => serialize_import_result(&result),
212            Err(err) => Err(import_error_to_js(err)),
213        }
214    }
215
216    /// Export a data model to SQL CREATE TABLE statements.
217    ///
218    /// # Arguments
219    ///
220    /// * `workspace_json` - JSON string containing workspace/data model structure
221    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", "databricks")
222    ///
223    /// # Returns
224    ///
225    /// SQL CREATE TABLE statements, or JsValue error
226    #[wasm_bindgen]
227    pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
228        let model = deserialize_workspace(workspace_json)?;
229        let exporter = crate::export::SQLExporter;
230        match exporter.export(&model.tables, Some(dialect)) {
231            Ok(result) => Ok(result.content),
232            Err(err) => Err(export_error_to_js(err)),
233        }
234    }
235
236    /// Export a data model to AVRO schema.
237    ///
238    /// # Arguments
239    ///
240    /// * `workspace_json` - JSON string containing workspace/data model structure
241    ///
242    /// # Returns
243    ///
244    /// AVRO schema JSON string, or JsValue error
245    #[wasm_bindgen]
246    pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
247        let model = deserialize_workspace(workspace_json)?;
248        let exporter = crate::export::AvroExporter;
249        match exporter.export(&model.tables) {
250            Ok(result) => Ok(result.content),
251            Err(err) => Err(export_error_to_js(err)),
252        }
253    }
254
255    /// Export a data model to JSON Schema definition.
256    ///
257    /// # Arguments
258    ///
259    /// * `workspace_json` - JSON string containing workspace/data model structure
260    ///
261    /// # Returns
262    ///
263    /// JSON Schema definition string, or JsValue error
264    #[wasm_bindgen]
265    pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
266        let model = deserialize_workspace(workspace_json)?;
267        let exporter = crate::export::JSONSchemaExporter;
268        match exporter.export(&model.tables) {
269            Ok(result) => Ok(result.content),
270            Err(err) => Err(export_error_to_js(err)),
271        }
272    }
273
274    /// Export a data model to Protobuf schema.
275    ///
276    /// # Arguments
277    ///
278    /// * `workspace_json` - JSON string containing workspace/data model structure
279    ///
280    /// # Returns
281    ///
282    /// Protobuf schema text, or JsValue error
283    #[wasm_bindgen]
284    pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
285        let model = deserialize_workspace(workspace_json)?;
286        let exporter = crate::export::ProtobufExporter;
287        match exporter.export(&model.tables) {
288            Ok(result) => Ok(result.content),
289            Err(err) => Err(export_error_to_js(err)),
290        }
291    }
292
293    // ============================================================================
294    // Validation Functions
295    // ============================================================================
296
297    /// Validate a table name.
298    ///
299    /// # Arguments
300    ///
301    /// * `name` - Table name to validate
302    ///
303    /// # Returns
304    ///
305    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
306    #[wasm_bindgen]
307    pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
308        match crate::validation::input::validate_table_name(name) {
309            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
310            Err(err) => {
311                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
312            }
313        }
314    }
315
316    /// Validate a column name.
317    ///
318    /// # Arguments
319    ///
320    /// * `name` - Column name to validate
321    ///
322    /// # Returns
323    ///
324    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
325    #[wasm_bindgen]
326    pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
327        match crate::validation::input::validate_column_name(name) {
328            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
329            Err(err) => {
330                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
331            }
332        }
333    }
334
335    /// Validate a UUID string.
336    ///
337    /// # Arguments
338    ///
339    /// * `id` - UUID string to validate
340    ///
341    /// # Returns
342    ///
343    /// JSON string with validation result: `{"valid": true, "uuid": "..."}` or `{"valid": false, "error": "error message"}`
344    #[wasm_bindgen]
345    pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
346        match crate::validation::input::validate_uuid(id) {
347            Ok(uuid) => {
348                Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
349            }
350            Err(err) => {
351                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
352            }
353        }
354    }
355
356    /// Validate a data type string.
357    ///
358    /// # Arguments
359    ///
360    /// * `data_type` - Data type string to validate
361    ///
362    /// # Returns
363    ///
364    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
365    #[wasm_bindgen]
366    pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
367        match crate::validation::input::validate_data_type(data_type) {
368            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
369            Err(err) => {
370                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
371            }
372        }
373    }
374
375    /// Validate a description string.
376    ///
377    /// # Arguments
378    ///
379    /// * `desc` - Description string to validate
380    ///
381    /// # Returns
382    ///
383    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
384    #[wasm_bindgen]
385    pub fn validate_description(desc: &str) -> Result<String, JsValue> {
386        match crate::validation::input::validate_description(desc) {
387            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
388            Err(err) => {
389                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
390            }
391        }
392    }
393
394    /// Sanitize a SQL identifier by quoting it.
395    ///
396    /// # Arguments
397    ///
398    /// * `name` - SQL identifier to sanitize
399    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", etc.)
400    ///
401    /// # Returns
402    ///
403    /// Sanitized SQL identifier string
404    #[wasm_bindgen]
405    pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
406        crate::validation::input::sanitize_sql_identifier(name, dialect)
407    }
408
409    /// Sanitize a description string.
410    ///
411    /// # Arguments
412    ///
413    /// * `desc` - Description string to sanitize
414    ///
415    /// # Returns
416    ///
417    /// Sanitized description string
418    #[wasm_bindgen]
419    pub fn sanitize_description(desc: &str) -> String {
420        crate::validation::input::sanitize_description(desc)
421    }
422
423    /// Detect naming conflicts between existing and new tables.
424    ///
425    /// # Arguments
426    ///
427    /// * `existing_tables_json` - JSON string containing array of existing tables
428    /// * `new_tables_json` - JSON string containing array of new tables
429    ///
430    /// # Returns
431    ///
432    /// JSON string containing array of naming conflicts
433    #[wasm_bindgen]
434    pub fn detect_naming_conflicts(
435        existing_tables_json: &str,
436        new_tables_json: &str,
437    ) -> Result<String, JsValue> {
438        let existing_tables: Vec<crate::models::Table> = serde_json::from_str(existing_tables_json)
439            .map_err(|e| JsValue::from_str(&format!("Failed to parse existing tables: {}", e)))?;
440        let new_tables: Vec<crate::models::Table> = serde_json::from_str(new_tables_json)
441            .map_err(|e| JsValue::from_str(&format!("Failed to parse new tables: {}", e)))?;
442
443        let validator = crate::validation::tables::TableValidator::new();
444        let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
445
446        serde_json::to_string(&conflicts)
447            .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
448    }
449
450    /// Validate pattern exclusivity for a table (SCD pattern and Data Vault classification are mutually exclusive).
451    ///
452    /// # Arguments
453    ///
454    /// * `table_json` - JSON string containing table to validate
455    ///
456    /// # Returns
457    ///
458    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "violation": {...}}`
459    #[wasm_bindgen]
460    pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
461        let table: crate::models::Table = serde_json::from_str(table_json)
462            .map_err(|e| JsValue::from_str(&format!("Failed to parse table: {}", e)))?;
463
464        let validator = crate::validation::tables::TableValidator::new();
465        match validator.validate_pattern_exclusivity(&table) {
466            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
467            Err(violation) => {
468                Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
469            }
470        }
471    }
472
473    /// Check for circular dependencies in relationships.
474    ///
475    /// # Arguments
476    ///
477    /// * `relationships_json` - JSON string containing array of existing relationships
478    /// * `source_table_id` - Source table ID (UUID string) of the new relationship
479    /// * `target_table_id` - Target table ID (UUID string) of the new relationship
480    ///
481    /// # Returns
482    ///
483    /// JSON string with result: `{"has_cycle": true/false, "cycle_path": [...]}` or error
484    #[wasm_bindgen]
485    pub fn check_circular_dependency(
486        relationships_json: &str,
487        source_table_id: &str,
488        target_table_id: &str,
489    ) -> Result<String, JsValue> {
490        let relationships: Vec<crate::models::Relationship> =
491            serde_json::from_str(relationships_json)
492                .map_err(|e| JsValue::from_str(&format!("Failed to parse relationships: {}", e)))?;
493
494        let source_id = uuid::Uuid::parse_str(source_table_id)
495            .map_err(|e| JsValue::from_str(&format!("Invalid source_table_id: {}", e)))?;
496        let target_id = uuid::Uuid::parse_str(target_table_id)
497            .map_err(|e| JsValue::from_str(&format!("Invalid target_table_id: {}", e)))?;
498
499        let validator = crate::validation::relationships::RelationshipValidator::new();
500        match validator.check_circular_dependency(&relationships, source_id, target_id) {
501            Ok((has_cycle, cycle_path)) => {
502                let cycle_path_strs: Vec<String> = cycle_path
503                    .map(|path| path.iter().map(|id| id.to_string()).collect())
504                    .unwrap_or_default();
505                Ok(serde_json::json!({
506                    "has_cycle": has_cycle,
507                    "cycle_path": cycle_path_strs
508                })
509                .to_string())
510            }
511            Err(err) => Err(JsValue::from_str(&format!("Validation error: {}", err))),
512        }
513    }
514
515    /// Validate that source and target tables are different (no self-reference).
516    ///
517    /// # Arguments
518    ///
519    /// * `source_table_id` - Source table ID (UUID string)
520    /// * `target_table_id` - Target table ID (UUID string)
521    ///
522    /// # Returns
523    ///
524    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "self_reference": {...}}`
525    #[wasm_bindgen]
526    pub fn validate_no_self_reference(
527        source_table_id: &str,
528        target_table_id: &str,
529    ) -> Result<String, JsValue> {
530        let source_id = uuid::Uuid::parse_str(source_table_id)
531            .map_err(|e| JsValue::from_str(&format!("Invalid source_table_id: {}", e)))?;
532        let target_id = uuid::Uuid::parse_str(target_table_id)
533            .map_err(|e| JsValue::from_str(&format!("Invalid target_table_id: {}", e)))?;
534
535        let validator = crate::validation::relationships::RelationshipValidator::new();
536        match validator.validate_no_self_reference(source_id, target_id) {
537            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
538            Err(self_ref) => {
539                Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
540            }
541        }
542    }
543
544    // ============================================================================
545    // PNG Export
546    // ============================================================================
547
548    /// Export a data model to PNG image format.
549    ///
550    /// # Arguments
551    ///
552    /// * `workspace_json` - JSON string containing workspace/data model structure
553    /// * `width` - Image width in pixels
554    /// * `height` - Image height in pixels
555    ///
556    /// # Returns
557    ///
558    /// Base64-encoded PNG image string, or JsValue error
559    #[cfg(feature = "png-export")]
560    #[wasm_bindgen]
561    pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
562        let model = deserialize_workspace(workspace_json)?;
563        let exporter = crate::export::PNGExporter::new();
564        match exporter.export(&model.tables, width, height) {
565            Ok(result) => Ok(result.content), // Already base64-encoded
566            Err(err) => Err(export_error_to_js(err)),
567        }
568    }
569
570    // ============================================================================
571    // Model Loading/Saving (Async)
572    // ============================================================================
573
574    /// Load a model from browser storage (IndexedDB/localStorage).
575    ///
576    /// # Arguments
577    ///
578    /// * `db_name` - IndexedDB database name
579    /// * `store_name` - Object store name
580    /// * `workspace_path` - Workspace path to load from
581    ///
582    /// # Returns
583    ///
584    /// Promise that resolves to JSON string containing ModelLoadResult, or rejects with error
585    #[wasm_bindgen]
586    pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
587        let db_name = db_name.to_string();
588        let store_name = store_name.to_string();
589        let workspace_path = workspace_path.to_string();
590
591        wasm_bindgen_futures::future_to_promise(async move {
592            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
593            let loader = crate::model::ModelLoader::new(storage);
594            match loader.load_model(&workspace_path).await {
595                Ok(result) => serde_json::to_string(&result)
596                    .map(|s| JsValue::from_str(&s))
597                    .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))),
598                Err(err) => Err(JsValue::from_str(&format!("Storage error: {}", err))),
599            }
600        })
601    }
602
603    /// Save a model to browser storage (IndexedDB/localStorage).
604    ///
605    /// # Arguments
606    ///
607    /// * `db_name` - IndexedDB database name
608    /// * `store_name` - Object store name
609    /// * `workspace_path` - Workspace path to save to
610    /// * `model_json` - JSON string containing DataModel to save
611    ///
612    /// # Returns
613    ///
614    /// Promise that resolves to success message, or rejects with error
615    #[wasm_bindgen]
616    pub fn save_model(
617        db_name: &str,
618        store_name: &str,
619        workspace_path: &str,
620        model_json: &str,
621    ) -> js_sys::Promise {
622        let db_name = db_name.to_string();
623        let store_name = store_name.to_string();
624        let workspace_path = workspace_path.to_string();
625        let model_json = model_json.to_string();
626
627        wasm_bindgen_futures::future_to_promise(async move {
628            let model: crate::models::DataModel = serde_json::from_str(&model_json)
629                .map_err(|e| JsValue::from_str(&format!("Failed to parse model: {}", e)))?;
630
631            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
632            let saver = crate::model::ModelSaver::new(storage);
633
634            // Convert DataModel to table/relationship data for saving
635            // For each table, save as YAML
636            for table in &model.tables {
637                // Export table to ODCS YAML
638                let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
639                let table_data = crate::model::saver::TableData {
640                    id: table.id,
641                    name: table.name.clone(),
642                    yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
643                    yaml_value: serde_yaml::from_str(&yaml)
644                        .map_err(|e| JsValue::from_str(&format!("Failed to parse YAML: {}", e)))?,
645                };
646                saver
647                    .save_table(&workspace_path, &table_data)
648                    .await
649                    .map_err(|e| JsValue::from_str(&format!("Failed to save table: {}", e)))?;
650            }
651
652            // Save relationships
653            if !model.relationships.is_empty() {
654                let rel_data: Vec<crate::model::saver::RelationshipData> = model
655                    .relationships
656                    .iter()
657                    .map(|rel| {
658                        let yaml_value = serde_json::json!({
659                            "id": rel.id.to_string(),
660                            "source_table_id": rel.source_table_id.to_string(),
661                            "target_table_id": rel.target_table_id.to_string(),
662                        });
663                        // Convert JSON value to YAML value
664                        let yaml_str = serde_json::to_string(&yaml_value)
665                            .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
666                        let yaml_value = serde_yaml::from_str(&yaml_str)
667                            .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
668                        Ok(crate::model::saver::RelationshipData {
669                            id: rel.id,
670                            source_table_id: rel.source_table_id,
671                            target_table_id: rel.target_table_id,
672                            yaml_value,
673                        })
674                    })
675                    .collect::<Result<Vec<_>, String>>()
676                    .map_err(|e| JsValue::from_str(&e))?;
677
678                saver
679                    .save_relationships(&workspace_path, &rel_data)
680                    .await
681                    .map_err(|e| {
682                        JsValue::from_str(&format!("Failed to save relationships: {}", e))
683                    })?;
684            }
685
686            Ok(JsValue::from_str("Model saved successfully"))
687        })
688    }
689}