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;
12#[cfg(feature = "cli")]
13pub mod cli;
14pub mod convert;
15#[cfg(feature = "database")]
16pub mod database;
17pub mod export;
18#[cfg(feature = "git")]
19pub mod git;
20pub mod import;
21pub mod model;
22pub mod models;
23pub mod storage;
24pub mod validation;
25pub mod workspace;
26
27// Re-export commonly used types
28#[cfg(feature = "api-backend")]
29pub use storage::api::ApiStorageBackend;
30#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
31pub use storage::browser::BrowserStorageBackend;
32#[cfg(feature = "native-fs")]
33pub use storage::filesystem::FileSystemStorageBackend;
34pub use storage::{StorageBackend, StorageError};
35
36pub use convert::{ConversionError, convert_to_odcs};
37#[cfg(feature = "png-export")]
38pub use export::PNGExporter;
39pub use export::{
40    AvroExporter, ExportError, ExportResult, JSONSchemaExporter, ODCSExporter, ProtobufExporter,
41    SQLExporter,
42};
43pub use import::{
44    AvroImporter, ImportError, ImportResult, JSONSchemaImporter, ODCSImporter, ProtobufImporter,
45    SQLImporter,
46};
47#[cfg(feature = "api-backend")]
48pub use model::ApiModelLoader;
49pub use model::{ModelLoader, ModelSaver};
50pub use validation::{
51    RelationshipValidationError, RelationshipValidationResult, TableValidationError,
52    TableValidationResult,
53};
54
55// Re-export models
56pub use models::enums::*;
57pub use models::{Column, ContactDetails, DataModel, ForeignKey, Relationship, SlaProperty, Table};
58
59// Re-export auth types
60pub use auth::{
61    AuthMode, AuthState, GitHubEmail, InitiateOAuthRequest, InitiateOAuthResponse,
62    SelectEmailRequest,
63};
64
65// Re-export workspace types
66pub use workspace::{
67    CreateWorkspaceRequest, CreateWorkspaceResponse, ListProfilesResponse, LoadProfileRequest,
68    ProfileInfo, WorkspaceInfo,
69};
70
71// Re-export Git types
72#[cfg(feature = "git")]
73pub use git::{GitError, GitService, GitStatus};
74
75// WASM bindings for import/export functions
76#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
77mod wasm {
78    use crate::export::ExportError;
79    use crate::import::{ImportError, ImportResult};
80    use crate::models::DataModel;
81    use js_sys;
82    use serde::{Deserialize, Serialize};
83    use serde_json;
84    use serde_yaml;
85    use uuid;
86    use wasm_bindgen::prelude::*;
87    use wasm_bindgen_futures;
88
89    /// Structured error type for WASM bindings.
90    /// Provides detailed error information that can be parsed by JavaScript consumers.
91    #[derive(Debug, Clone, Serialize, Deserialize)]
92    pub struct WasmError {
93        /// Error category (e.g., "ImportError", "ExportError", "ValidationError")
94        pub error_type: String,
95        /// Human-readable error message
96        pub message: String,
97        /// Optional error code for programmatic handling
98        #[serde(skip_serializing_if = "Option::is_none")]
99        pub code: Option<String>,
100        /// Optional additional details
101        #[serde(skip_serializing_if = "Option::is_none")]
102        pub details: Option<serde_json::Value>,
103    }
104
105    impl WasmError {
106        /// Create a new WasmError with the given type and message
107        fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
108            Self {
109                error_type: error_type.into(),
110                message: message.into(),
111                code: None,
112                details: None,
113            }
114        }
115
116        /// Create a WasmError with a specific error code
117        fn with_code(mut self, code: impl Into<String>) -> Self {
118            self.code = Some(code.into());
119            self
120        }
121
122        /// Convert to JsValue for returning to JavaScript
123        fn to_js_value(&self) -> JsValue {
124            // Serialize to JSON string for structured error handling in JS
125            match serde_json::to_string(self) {
126                Ok(json) => JsValue::from_str(&json),
127                // Fallback to simple message if serialization fails
128                Err(_) => JsValue::from_str(&self.message),
129            }
130        }
131    }
132
133    /// Convert ImportError to structured JsValue for JavaScript error handling
134    fn import_error_to_js(err: ImportError) -> JsValue {
135        WasmError::new("ImportError", err.to_string())
136            .with_code("IMPORT_FAILED")
137            .to_js_value()
138    }
139
140    /// Convert ExportError to structured JsValue for JavaScript error handling
141    fn export_error_to_js(err: ExportError) -> JsValue {
142        WasmError::new("ExportError", err.to_string())
143            .with_code("EXPORT_FAILED")
144            .to_js_value()
145    }
146
147    /// Create a serialization error
148    fn serialization_error(err: impl std::fmt::Display) -> JsValue {
149        WasmError::new(
150            "SerializationError",
151            format!("Serialization error: {}", err),
152        )
153        .with_code("SERIALIZATION_FAILED")
154        .to_js_value()
155    }
156
157    /// Create a deserialization error
158    fn deserialization_error(err: impl std::fmt::Display) -> JsValue {
159        WasmError::new(
160            "DeserializationError",
161            format!("Deserialization error: {}", err),
162        )
163        .with_code("DESERIALIZATION_FAILED")
164        .to_js_value()
165    }
166
167    /// Create a parse error
168    fn parse_error(err: impl std::fmt::Display) -> JsValue {
169        WasmError::new("ParseError", format!("Parse error: {}", err))
170            .with_code("PARSE_FAILED")
171            .to_js_value()
172    }
173
174    /// Create a validation error
175    fn validation_error(err: impl std::fmt::Display) -> JsValue {
176        WasmError::new("ValidationError", err.to_string())
177            .with_code("VALIDATION_FAILED")
178            .to_js_value()
179    }
180
181    /// Create an invalid input error
182    fn invalid_input_error(field: &str, err: impl std::fmt::Display) -> JsValue {
183        WasmError::new("InvalidInputError", format!("Invalid {}: {}", field, err))
184            .with_code("INVALID_INPUT")
185            .to_js_value()
186    }
187
188    /// Create a conversion error
189    fn conversion_error(err: impl std::fmt::Display) -> JsValue {
190        WasmError::new("ConversionError", format!("Conversion error: {}", err))
191            .with_code("CONVERSION_FAILED")
192            .to_js_value()
193    }
194
195    /// Create a storage error
196    fn storage_error(err: impl std::fmt::Display) -> JsValue {
197        WasmError::new("StorageError", format!("Storage error: {}", err))
198            .with_code("STORAGE_FAILED")
199            .to_js_value()
200    }
201
202    /// Serialize ImportResult to JSON string
203    fn serialize_import_result(result: &ImportResult) -> Result<String, JsValue> {
204        serde_json::to_string(result).map_err(serialization_error)
205    }
206
207    /// Flatten STRUCT columns in ImportResult into nested columns with dot notation
208    ///
209    /// This processes each table's columns and expands STRUCT types into individual
210    /// columns with parent.child naming:
211    /// - STRUCT<field1: TYPE1, field2: TYPE2> → parent.field1, parent.field2
212    /// - ARRAY<STRUCT<...>> → parent.[].field1, parent.[].field2
213    /// - MAP types are kept as-is (keys are dynamic)
214    fn flatten_struct_columns(result: ImportResult) -> ImportResult {
215        use crate::import::{ColumnData, ODCSImporter, TableData};
216
217        let importer = ODCSImporter::new();
218
219        let tables = result
220            .tables
221            .into_iter()
222            .map(|table_data| {
223                let mut all_columns = Vec::new();
224
225                for col_data in table_data.columns {
226                    let data_type_upper = col_data.data_type.to_uppercase();
227                    let is_map = data_type_upper.starts_with("MAP<");
228
229                    // Skip parsing for MAP types - keys are dynamic
230                    if is_map {
231                        all_columns.push(col_data);
232                        continue;
233                    }
234
235                    // For STRUCT or ARRAY<STRUCT> types, try to parse and create nested columns
236                    let is_struct = data_type_upper.contains("STRUCT<");
237                    if is_struct {
238                        let field_data = serde_json::Map::new();
239                        if let Ok(nested_cols) = importer.parse_struct_type_from_string(
240                            &col_data.name,
241                            &col_data.data_type,
242                            &field_data,
243                        ) {
244                            if !nested_cols.is_empty() {
245                                // Add parent column with simplified type
246                                let parent_data_type =
247                                    if col_data.data_type.to_uppercase().starts_with("ARRAY<") {
248                                        "ARRAY<STRUCT<...>>".to_string()
249                                    } else {
250                                        "STRUCT<...>".to_string()
251                                    };
252
253                                all_columns.push(ColumnData {
254                                    name: col_data.name.clone(),
255                                    data_type: parent_data_type,
256                                    physical_type: col_data.physical_type.clone(),
257                                    nullable: col_data.nullable,
258                                    primary_key: col_data.primary_key,
259                                    description: col_data.description.clone(),
260                                    quality: col_data.quality.clone(),
261                                    relationships: col_data.relationships.clone(),
262                                    enum_values: col_data.enum_values.clone(),
263                                    ..Default::default()
264                                });
265
266                                // Add nested columns converted from Column to ColumnData
267                                for nested_col in nested_cols {
268                                    all_columns.push(
269                                        crate::import::odcs_shared::column_to_column_data(
270                                            &nested_col,
271                                        ),
272                                    );
273                                }
274                                continue;
275                            }
276                        }
277                    }
278
279                    // Regular column or STRUCT parsing failed - add as-is
280                    all_columns.push(col_data);
281                }
282
283                TableData {
284                    table_index: table_data.table_index,
285                    name: table_data.name,
286                    columns: all_columns,
287                }
288            })
289            .collect();
290
291        ImportResult {
292            tables,
293            tables_requiring_name: result.tables_requiring_name,
294            errors: result.errors,
295            ai_suggestions: result.ai_suggestions,
296        }
297    }
298
299    /// Deserialize workspace structure from JSON string
300    fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
301        serde_json::from_str(json).map_err(deserialization_error)
302    }
303
304    /// Parse ODCS YAML content and return a structured workspace representation.
305    ///
306    /// # Arguments
307    ///
308    /// * `yaml_content` - ODCS YAML content as a string
309    ///
310    /// # Returns
311    ///
312    /// JSON string containing ImportResult object, or JsValue error
313    #[wasm_bindgen]
314    pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
315        let mut importer = crate::import::ODCSImporter::new();
316        match importer.import(yaml_content) {
317            Ok(result) => {
318                let flattened = flatten_struct_columns(result);
319                serialize_import_result(&flattened)
320            }
321            Err(err) => Err(import_error_to_js(err)),
322        }
323    }
324
325    /// Import data model from legacy ODCL (Open Data Contract Language) YAML format.
326    ///
327    /// This function parses legacy ODCL formats including:
328    /// - Data Contract Specification format (dataContractSpecification, models, definitions)
329    /// - Simple ODCL format (name, columns)
330    ///
331    /// For ODCS v3.1.0/v3.0.x format, use `parse_odcs_yaml` instead.
332    ///
333    /// # Arguments
334    ///
335    /// * `yaml_content` - ODCL YAML content as a string
336    ///
337    /// # Returns
338    ///
339    /// JSON string containing ImportResult object, or JsValue error
340    #[wasm_bindgen]
341    pub fn parse_odcl_yaml(yaml_content: &str) -> Result<String, JsValue> {
342        let mut importer = crate::import::ODCLImporter::new();
343        match importer.import(yaml_content) {
344            Ok(result) => {
345                let flattened = flatten_struct_columns(result);
346                serialize_import_result(&flattened)
347            }
348            Err(err) => Err(import_error_to_js(err)),
349        }
350    }
351
352    /// Check if the given YAML content is in legacy ODCL format.
353    ///
354    /// Returns true if the content is in ODCL format (Data Contract Specification
355    /// or simple ODCL format), false if it's in ODCS v3.x format or invalid.
356    ///
357    /// # Arguments
358    ///
359    /// * `yaml_content` - YAML content to check
360    ///
361    /// # Returns
362    ///
363    /// Boolean indicating if the content is ODCL format
364    #[wasm_bindgen]
365    pub fn is_odcl_format(yaml_content: &str) -> bool {
366        let importer = crate::import::ODCLImporter::new();
367        importer.can_handle(yaml_content)
368    }
369
370    /// Export a workspace structure to ODCS YAML format.
371    ///
372    /// # Arguments
373    ///
374    /// * `workspace_json` - JSON string containing workspace/data model structure
375    ///
376    /// # Returns
377    ///
378    /// ODCS YAML format string, or JsValue error
379    #[wasm_bindgen]
380    pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
381        let model = deserialize_workspace(workspace_json)?;
382
383        // Export all tables as separate YAML documents, joined with ---\n
384        let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
385
386        // Combine all YAML documents into a single multi-document string
387        let yaml_docs: Vec<String> = exports.values().cloned().collect();
388        Ok(yaml_docs.join("\n---\n"))
389    }
390
391    /// Import data model from SQL CREATE TABLE statements.
392    ///
393    /// # Arguments
394    ///
395    /// * `sql_content` - SQL CREATE TABLE statements
396    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", "databricks")
397    ///
398    /// # Returns
399    ///
400    /// JSON string containing ImportResult object, or JsValue error
401    #[wasm_bindgen]
402    pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
403        let importer = crate::import::SQLImporter::new(dialect);
404        match importer.parse(sql_content) {
405            Ok(result) => {
406                // Flatten STRUCT columns into nested columns with dot notation
407                let flattened = flatten_struct_columns(result);
408                serialize_import_result(&flattened)
409            }
410            Err(err) => Err(parse_error(err)),
411        }
412    }
413
414    /// Import data model from AVRO schema.
415    ///
416    /// # Arguments
417    ///
418    /// * `avro_content` - AVRO schema JSON as a string
419    ///
420    /// # Returns
421    ///
422    /// JSON string containing ImportResult object, or JsValue error
423    #[wasm_bindgen]
424    pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
425        let importer = crate::import::AvroImporter::new();
426        match importer.import(avro_content) {
427            Ok(result) => {
428                let flattened = flatten_struct_columns(result);
429                serialize_import_result(&flattened)
430            }
431            Err(err) => Err(import_error_to_js(err)),
432        }
433    }
434
435    /// Import data model from JSON Schema definition.
436    ///
437    /// # Arguments
438    ///
439    /// * `json_schema_content` - JSON Schema definition as a string
440    ///
441    /// # Returns
442    ///
443    /// JSON string containing ImportResult object, or JsValue error
444    #[wasm_bindgen]
445    pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
446        let importer = crate::import::JSONSchemaImporter::new();
447        match importer.import(json_schema_content) {
448            Ok(result) => {
449                let flattened = flatten_struct_columns(result);
450                serialize_import_result(&flattened)
451            }
452            Err(err) => Err(import_error_to_js(err)),
453        }
454    }
455
456    /// Import data model from Protobuf schema.
457    ///
458    /// # Arguments
459    ///
460    /// * `protobuf_content` - Protobuf schema text
461    ///
462    /// # Returns
463    ///
464    /// JSON string containing ImportResult object, or JsValue error
465    #[wasm_bindgen]
466    pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
467        let importer = crate::import::ProtobufImporter::new();
468        match importer.import(protobuf_content) {
469            Ok(result) => {
470                let flattened = flatten_struct_columns(result);
471                serialize_import_result(&flattened)
472            }
473            Err(err) => Err(import_error_to_js(err)),
474        }
475    }
476
477    /// Export a data model to SQL CREATE TABLE statements.
478    ///
479    /// # Arguments
480    ///
481    /// * `workspace_json` - JSON string containing workspace/data model structure
482    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", "databricks")
483    ///
484    /// # Returns
485    ///
486    /// SQL CREATE TABLE statements, or JsValue error
487    #[wasm_bindgen]
488    pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
489        let model = deserialize_workspace(workspace_json)?;
490        let exporter = crate::export::SQLExporter;
491        match exporter.export(&model.tables, Some(dialect)) {
492            Ok(result) => Ok(result.content),
493            Err(err) => Err(export_error_to_js(err)),
494        }
495    }
496
497    /// Export a data model to AVRO schema.
498    ///
499    /// # Arguments
500    ///
501    /// * `workspace_json` - JSON string containing workspace/data model structure
502    ///
503    /// # Returns
504    ///
505    /// AVRO schema JSON string, or JsValue error
506    #[wasm_bindgen]
507    pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
508        let model = deserialize_workspace(workspace_json)?;
509        let exporter = crate::export::AvroExporter;
510        match exporter.export(&model.tables) {
511            Ok(result) => Ok(result.content),
512            Err(err) => Err(export_error_to_js(err)),
513        }
514    }
515
516    /// Export a data model to JSON Schema definition.
517    ///
518    /// # Arguments
519    ///
520    /// * `workspace_json` - JSON string containing workspace/data model structure
521    ///
522    /// # Returns
523    ///
524    /// JSON Schema definition string, or JsValue error
525    #[wasm_bindgen]
526    pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
527        let model = deserialize_workspace(workspace_json)?;
528        let exporter = crate::export::JSONSchemaExporter;
529        match exporter.export(&model.tables) {
530            Ok(result) => Ok(result.content),
531            Err(err) => Err(export_error_to_js(err)),
532        }
533    }
534
535    /// Export a data model to Protobuf schema.
536    ///
537    /// # Arguments
538    ///
539    /// * `workspace_json` - JSON string containing workspace/data model structure
540    ///
541    /// # Returns
542    ///
543    /// Protobuf schema text, or JsValue error
544    #[wasm_bindgen]
545    pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
546        let model = deserialize_workspace(workspace_json)?;
547        let exporter = crate::export::ProtobufExporter;
548        match exporter.export(&model.tables) {
549            Ok(result) => Ok(result.content),
550            Err(err) => Err(export_error_to_js(err)),
551        }
552    }
553
554    /// Import CADS YAML content and return a structured representation.
555    ///
556    /// # Arguments
557    ///
558    /// * `yaml_content` - CADS YAML content as a string
559    ///
560    /// # Returns
561    ///
562    /// JSON string containing CADS asset, or JsValue error
563    #[wasm_bindgen]
564    pub fn import_from_cads(yaml_content: &str) -> Result<String, JsValue> {
565        let importer = crate::import::CADSImporter::new();
566        match importer.import(yaml_content) {
567            Ok(asset) => serde_json::to_string(&asset).map_err(serialization_error),
568            Err(err) => Err(import_error_to_js(err)),
569        }
570    }
571
572    /// Export a CADS asset to YAML format.
573    ///
574    /// # Arguments
575    ///
576    /// * `asset_json` - JSON string containing CADS asset
577    ///
578    /// # Returns
579    ///
580    /// CADS YAML format string, or JsValue error
581    #[wasm_bindgen]
582    pub fn export_to_cads(asset_json: &str) -> Result<String, JsValue> {
583        let asset: crate::models::cads::CADSAsset =
584            serde_json::from_str(asset_json).map_err(deserialization_error)?;
585        let exporter = crate::export::CADSExporter;
586        match exporter.export(&asset) {
587            Ok(yaml) => Ok(yaml),
588            Err(err) => Err(export_error_to_js(err)),
589        }
590    }
591
592    /// Import ODPS YAML content and return a structured representation.
593    ///
594    /// # Arguments
595    ///
596    /// * `yaml_content` - ODPS YAML content as a string
597    ///
598    /// # Returns
599    ///
600    /// JSON string containing ODPS data product, or JsValue error
601    #[wasm_bindgen]
602    pub fn import_from_odps(yaml_content: &str) -> Result<String, JsValue> {
603        let importer = crate::import::ODPSImporter::new();
604        match importer.import(yaml_content) {
605            Ok(product) => serde_json::to_string(&product).map_err(serialization_error),
606            Err(err) => Err(import_error_to_js(err)),
607        }
608    }
609
610    /// Export an ODPS data product to YAML format.
611    ///
612    /// # Arguments
613    ///
614    /// * `product_json` - JSON string containing ODPS data product
615    ///
616    /// # Returns
617    ///
618    /// ODPS YAML format string, or JsValue error
619    #[wasm_bindgen]
620    pub fn export_to_odps(product_json: &str) -> Result<String, JsValue> {
621        let product: crate::models::odps::ODPSDataProduct =
622            serde_json::from_str(product_json).map_err(deserialization_error)?;
623        let exporter = crate::export::ODPSExporter;
624        match exporter.export(&product) {
625            Ok(yaml) => Ok(yaml),
626            Err(err) => Err(export_error_to_js(err)),
627        }
628    }
629
630    /// Validate ODPS YAML content against the ODPS JSON Schema.
631    ///
632    /// # Arguments
633    ///
634    /// * `yaml_content` - ODPS YAML content as a string
635    ///
636    /// # Returns
637    ///
638    /// Empty string on success, or error message string
639    #[cfg(feature = "odps-validation")]
640    #[wasm_bindgen]
641    pub fn validate_odps(yaml_content: &str) -> Result<(), JsValue> {
642        #[cfg(feature = "cli")]
643        {
644            use crate::cli::validation::validate_odps_internal;
645            validate_odps_internal(yaml_content).map_err(validation_error)
646        }
647        #[cfg(not(feature = "cli"))]
648        {
649            // Inline validation when CLI feature is not enabled
650            use jsonschema::Validator;
651            use serde_json::Value;
652
653            let schema_content = include_str!("../schemas/odps-json-schema-latest.json");
654            let schema: Value = serde_json::from_str(schema_content)
655                .map_err(|e| validation_error(format!("Failed to load ODPS schema: {}", e)))?;
656
657            let validator = Validator::new(&schema)
658                .map_err(|e| validation_error(format!("Failed to compile ODPS schema: {}", e)))?;
659
660            let data: Value = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
661
662            if let Err(error) = validator.validate(&data) {
663                return Err(validation_error(format!(
664                    "ODPS validation failed: {}",
665                    error
666                )));
667            }
668
669            Ok(())
670        }
671    }
672
673    #[cfg(not(feature = "odps-validation"))]
674    #[wasm_bindgen]
675    pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
676        // Validation disabled - feature not enabled
677        // Return success to maintain backward compatibility
678        Ok(())
679    }
680
681    /// Create a new business domain.
682    ///
683    /// # Arguments
684    ///
685    /// * `name` - Domain name
686    ///
687    /// # Returns
688    ///
689    /// JSON string containing Domain, or JsValue error
690    #[wasm_bindgen]
691    pub fn create_domain(name: &str) -> Result<String, JsValue> {
692        let domain = crate::models::domain::Domain::new(name.to_string());
693        serde_json::to_string(&domain).map_err(serialization_error)
694    }
695
696    /// Import Domain YAML content and return a structured representation.
697    ///
698    /// # Arguments
699    ///
700    /// * `yaml_content` - Domain YAML content as a string
701    ///
702    /// # Returns
703    ///
704    /// JSON string containing Domain, or JsValue error
705    #[wasm_bindgen]
706    pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
707        match crate::models::domain::Domain::from_yaml(yaml_content) {
708            Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
709            Err(e) => Err(parse_error(e)),
710        }
711    }
712
713    /// Export a Domain to YAML format.
714    ///
715    /// # Arguments
716    ///
717    /// * `domain_json` - JSON string containing Domain
718    ///
719    /// # Returns
720    ///
721    /// Domain YAML format string, or JsValue error
722    #[wasm_bindgen]
723    pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
724        let domain: crate::models::domain::Domain =
725            serde_json::from_str(domain_json).map_err(deserialization_error)?;
726        domain.to_yaml().map_err(serialization_error)
727    }
728
729    /// Migrate DataFlow YAML to Domain schema format.
730    ///
731    /// # Arguments
732    ///
733    /// * `dataflow_yaml` - DataFlow YAML content as a string
734    /// * `domain_name` - Optional domain name (defaults to "MigratedDomain")
735    ///
736    /// # Returns
737    ///
738    /// JSON string containing Domain, or JsValue error
739    #[wasm_bindgen]
740    pub fn migrate_dataflow_to_domain(
741        dataflow_yaml: &str,
742        domain_name: Option<String>,
743    ) -> Result<String, JsValue> {
744        match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
745            dataflow_yaml,
746            domain_name.as_deref(),
747        ) {
748            Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
749            Err(e) => Err(conversion_error(e)),
750        }
751    }
752
753    /// Parse a tag string into a Tag enum.
754    ///
755    /// # Arguments
756    ///
757    /// * `tag_str` - Tag string (Simple, Pair, or List format)
758    ///
759    /// # Returns
760    ///
761    /// JSON string containing Tag, or JsValue error
762    #[wasm_bindgen]
763    pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
764        use crate::models::Tag;
765        use std::str::FromStr;
766        match Tag::from_str(tag_str) {
767            Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
768            Err(_) => Err(parse_error("Invalid tag format")),
769        }
770    }
771
772    /// Serialize a Tag enum to string format.
773    ///
774    /// # Arguments
775    ///
776    /// * `tag_json` - JSON string containing Tag
777    ///
778    /// # Returns
779    ///
780    /// Tag string (Simple, Pair, or List format), or JsValue error
781    #[wasm_bindgen]
782    pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
783        use crate::models::Tag;
784        let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
785        Ok(tag.to_string())
786    }
787
788    /// Convert any format to ODCS v3.1.0 YAML format.
789    ///
790    /// # Arguments
791    ///
792    /// * `input` - Format-specific content as a string
793    /// * `format` - Optional format identifier. If None, attempts auto-detection.
794    ///   Supported formats: "sql", "json_schema", "avro", "protobuf", "odcl", "odcs", "cads", "odps", "domain"
795    ///
796    /// # Returns
797    ///
798    /// ODCS v3.1.0 YAML string, or JsValue error
799    #[wasm_bindgen]
800    pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
801        match crate::convert::convert_to_odcs(input, format.as_deref()) {
802            Ok(yaml) => Ok(yaml),
803            Err(e) => Err(conversion_error(e)),
804        }
805    }
806
807    /// Filter Data Flow nodes (tables) by owner.
808    ///
809    /// # Arguments
810    ///
811    /// * `workspace_json` - JSON string containing workspace/data model structure
812    /// * `owner` - Owner name to filter by (case-sensitive exact match)
813    ///
814    /// # Returns
815    ///
816    /// JSON string containing array of matching tables, or JsValue error
817    #[wasm_bindgen]
818    pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
819        let model = deserialize_workspace(workspace_json)?;
820        let filtered = model.filter_nodes_by_owner(owner);
821        serde_json::to_string(&filtered).map_err(serialization_error)
822    }
823
824    /// Filter Data Flow relationships by owner.
825    ///
826    /// # Arguments
827    ///
828    /// * `workspace_json` - JSON string containing workspace/data model structure
829    /// * `owner` - Owner name to filter by (case-sensitive exact match)
830    ///
831    /// # Returns
832    ///
833    /// JSON string containing array of matching relationships, or JsValue error
834    #[wasm_bindgen]
835    pub fn filter_relationships_by_owner(
836        workspace_json: &str,
837        owner: &str,
838    ) -> Result<String, JsValue> {
839        let model = deserialize_workspace(workspace_json)?;
840        let filtered = model.filter_relationships_by_owner(owner);
841        serde_json::to_string(&filtered).map_err(serialization_error)
842    }
843
844    /// Filter Data Flow nodes (tables) by infrastructure type.
845    ///
846    /// # Arguments
847    ///
848    /// * `workspace_json` - JSON string containing workspace/data model structure
849    /// * `infrastructure_type` - Infrastructure type string (e.g., "Kafka", "PostgreSQL")
850    ///
851    /// # Returns
852    ///
853    /// JSON string containing array of matching tables, or JsValue error
854    #[wasm_bindgen]
855    pub fn filter_nodes_by_infrastructure_type(
856        workspace_json: &str,
857        infrastructure_type: &str,
858    ) -> Result<String, JsValue> {
859        let model = deserialize_workspace(workspace_json)?;
860        let infra_type: crate::models::enums::InfrastructureType =
861            serde_json::from_str(&format!("\"{}\"", infrastructure_type))
862                .map_err(|e| invalid_input_error("infrastructure type", e))?;
863        let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
864        serde_json::to_string(&filtered).map_err(serialization_error)
865    }
866
867    /// Filter Data Flow relationships by infrastructure type.
868    ///
869    /// # Arguments
870    ///
871    /// * `workspace_json` - JSON string containing workspace/data model structure
872    /// * `infrastructure_type` - Infrastructure type string (e.g., "Kafka", "PostgreSQL")
873    ///
874    /// # Returns
875    ///
876    /// JSON string containing array of matching relationships, or JsValue error
877    #[wasm_bindgen]
878    pub fn filter_relationships_by_infrastructure_type(
879        workspace_json: &str,
880        infrastructure_type: &str,
881    ) -> Result<String, JsValue> {
882        let model = deserialize_workspace(workspace_json)?;
883        let infra_type: crate::models::enums::InfrastructureType =
884            serde_json::from_str(&format!("\"{}\"", infrastructure_type))
885                .map_err(|e| invalid_input_error("infrastructure type", e))?;
886        let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
887        serde_json::to_string(&filtered).map_err(serialization_error)
888    }
889
890    /// Filter Data Flow nodes and relationships by tag.
891    ///
892    /// # Arguments
893    ///
894    /// * `workspace_json` - JSON string containing workspace/data model structure
895    /// * `tag` - Tag to filter by
896    ///
897    /// # Returns
898    ///
899    /// JSON string containing object with `nodes` and `relationships` arrays, or JsValue error
900    #[wasm_bindgen]
901    pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
902        let model = deserialize_workspace(workspace_json)?;
903        let (nodes, relationships) = model.filter_by_tags(tag);
904        let result = serde_json::json!({
905            "nodes": nodes,
906            "relationships": relationships
907        });
908        serde_json::to_string(&result).map_err(serialization_error)
909    }
910
911    // ============================================================================
912    // Domain Operations
913    // ============================================================================
914
915    /// Add a system to a domain in a DataModel.
916    ///
917    /// # Arguments
918    ///
919    /// * `workspace_json` - JSON string containing workspace/data model structure
920    /// * `domain_id` - Domain UUID as string
921    /// * `system_json` - JSON string containing System
922    ///
923    /// # Returns
924    ///
925    /// JSON string containing updated DataModel, or JsValue error
926    #[wasm_bindgen]
927    pub fn add_system_to_domain(
928        workspace_json: &str,
929        domain_id: &str,
930        system_json: &str,
931    ) -> Result<String, JsValue> {
932        let mut model = deserialize_workspace(workspace_json)?;
933        let domain_uuid =
934            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
935        let system: crate::models::domain::System =
936            serde_json::from_str(system_json).map_err(deserialization_error)?;
937        model
938            .add_system_to_domain(domain_uuid, system)
939            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
940        serde_json::to_string(&model).map_err(serialization_error)
941    }
942
943    /// Add a CADS node to a domain in a DataModel.
944    ///
945    /// # Arguments
946    ///
947    /// * `workspace_json` - JSON string containing workspace/data model structure
948    /// * `domain_id` - Domain UUID as string
949    /// * `node_json` - JSON string containing CADSNode
950    ///
951    /// # Returns
952    ///
953    /// JSON string containing updated DataModel, or JsValue error
954    #[wasm_bindgen]
955    pub fn add_cads_node_to_domain(
956        workspace_json: &str,
957        domain_id: &str,
958        node_json: &str,
959    ) -> Result<String, JsValue> {
960        let mut model = deserialize_workspace(workspace_json)?;
961        let domain_uuid =
962            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
963        let node: crate::models::domain::CADSNode =
964            serde_json::from_str(node_json).map_err(deserialization_error)?;
965        model
966            .add_cads_node_to_domain(domain_uuid, node)
967            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
968        serde_json::to_string(&model).map_err(serialization_error)
969    }
970
971    /// Add an ODCS node to a domain in a DataModel.
972    ///
973    /// # Arguments
974    ///
975    /// * `workspace_json` - JSON string containing workspace/data model structure
976    /// * `domain_id` - Domain UUID as string
977    /// * `node_json` - JSON string containing ODCSNode
978    ///
979    /// # Returns
980    ///
981    /// JSON string containing updated DataModel, or JsValue error
982    #[wasm_bindgen]
983    pub fn add_odcs_node_to_domain(
984        workspace_json: &str,
985        domain_id: &str,
986        node_json: &str,
987    ) -> Result<String, JsValue> {
988        let mut model = deserialize_workspace(workspace_json)?;
989        let domain_uuid =
990            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
991        let node: crate::models::domain::ODCSNode =
992            serde_json::from_str(node_json).map_err(deserialization_error)?;
993        model
994            .add_odcs_node_to_domain(domain_uuid, node)
995            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
996        serde_json::to_string(&model).map_err(serialization_error)
997    }
998
999    // ============================================================================
1000    // Validation Functions
1001    // ============================================================================
1002
1003    /// Validate a table name.
1004    ///
1005    /// # Arguments
1006    ///
1007    /// * `name` - Table name to validate
1008    ///
1009    /// # Returns
1010    ///
1011    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1012    #[wasm_bindgen]
1013    pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
1014        match crate::validation::input::validate_table_name(name) {
1015            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1016            Err(err) => {
1017                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1018            }
1019        }
1020    }
1021
1022    /// Validate a column name.
1023    ///
1024    /// # Arguments
1025    ///
1026    /// * `name` - Column name to validate
1027    ///
1028    /// # Returns
1029    ///
1030    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1031    #[wasm_bindgen]
1032    pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
1033        match crate::validation::input::validate_column_name(name) {
1034            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1035            Err(err) => {
1036                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1037            }
1038        }
1039    }
1040
1041    /// Validate a UUID string.
1042    ///
1043    /// # Arguments
1044    ///
1045    /// * `id` - UUID string to validate
1046    ///
1047    /// # Returns
1048    ///
1049    /// JSON string with validation result: `{"valid": true, "uuid": "..."}` or `{"valid": false, "error": "error message"}`
1050    #[wasm_bindgen]
1051    pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
1052        match crate::validation::input::validate_uuid(id) {
1053            Ok(uuid) => {
1054                Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
1055            }
1056            Err(err) => {
1057                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1058            }
1059        }
1060    }
1061
1062    /// Validate a data type string.
1063    ///
1064    /// # Arguments
1065    ///
1066    /// * `data_type` - Data type string to validate
1067    ///
1068    /// # Returns
1069    ///
1070    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1071    #[wasm_bindgen]
1072    pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
1073        match crate::validation::input::validate_data_type(data_type) {
1074            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1075            Err(err) => {
1076                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1077            }
1078        }
1079    }
1080
1081    /// Validate a description string.
1082    ///
1083    /// # Arguments
1084    ///
1085    /// * `desc` - Description string to validate
1086    ///
1087    /// # Returns
1088    ///
1089    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1090    #[wasm_bindgen]
1091    pub fn validate_description(desc: &str) -> Result<String, JsValue> {
1092        match crate::validation::input::validate_description(desc) {
1093            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1094            Err(err) => {
1095                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1096            }
1097        }
1098    }
1099
1100    /// Sanitize a SQL identifier by quoting it.
1101    ///
1102    /// # Arguments
1103    ///
1104    /// * `name` - SQL identifier to sanitize
1105    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", etc.)
1106    ///
1107    /// # Returns
1108    ///
1109    /// Sanitized SQL identifier string
1110    #[wasm_bindgen]
1111    pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
1112        crate::validation::input::sanitize_sql_identifier(name, dialect)
1113    }
1114
1115    /// Sanitize a description string.
1116    ///
1117    /// # Arguments
1118    ///
1119    /// * `desc` - Description string to sanitize
1120    ///
1121    /// # Returns
1122    ///
1123    /// Sanitized description string
1124    #[wasm_bindgen]
1125    pub fn sanitize_description(desc: &str) -> String {
1126        crate::validation::input::sanitize_description(desc)
1127    }
1128
1129    /// Detect naming conflicts between existing and new tables.
1130    ///
1131    /// # Arguments
1132    ///
1133    /// * `existing_tables_json` - JSON string containing array of existing tables
1134    /// * `new_tables_json` - JSON string containing array of new tables
1135    ///
1136    /// # Returns
1137    ///
1138    /// JSON string containing array of naming conflicts
1139    #[wasm_bindgen]
1140    pub fn detect_naming_conflicts(
1141        existing_tables_json: &str,
1142        new_tables_json: &str,
1143    ) -> Result<String, JsValue> {
1144        let existing_tables: Vec<crate::models::Table> =
1145            serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1146        let new_tables: Vec<crate::models::Table> =
1147            serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1148
1149        let validator = crate::validation::tables::TableValidator::new();
1150        let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1151
1152        serde_json::to_string(&conflicts).map_err(serialization_error)
1153    }
1154
1155    /// Validate pattern exclusivity for a table (SCD pattern and Data Vault classification are mutually exclusive).
1156    ///
1157    /// # Arguments
1158    ///
1159    /// * `table_json` - JSON string containing table to validate
1160    ///
1161    /// # Returns
1162    ///
1163    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "violation": {...}}`
1164    #[wasm_bindgen]
1165    pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1166        let table: crate::models::Table =
1167            serde_json::from_str(table_json).map_err(deserialization_error)?;
1168
1169        let validator = crate::validation::tables::TableValidator::new();
1170        match validator.validate_pattern_exclusivity(&table) {
1171            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1172            Err(violation) => {
1173                Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1174            }
1175        }
1176    }
1177
1178    /// Check for circular dependencies in relationships.
1179    ///
1180    /// # Arguments
1181    ///
1182    /// * `relationships_json` - JSON string containing array of existing relationships
1183    /// * `source_table_id` - Source table ID (UUID string) of the new relationship
1184    /// * `target_table_id` - Target table ID (UUID string) of the new relationship
1185    ///
1186    /// # Returns
1187    ///
1188    /// JSON string with result: `{"has_cycle": true/false, "cycle_path": [...]}` or error
1189    #[wasm_bindgen]
1190    pub fn check_circular_dependency(
1191        relationships_json: &str,
1192        source_table_id: &str,
1193        target_table_id: &str,
1194    ) -> Result<String, JsValue> {
1195        let relationships: Vec<crate::models::Relationship> =
1196            serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1197
1198        let source_id = uuid::Uuid::parse_str(source_table_id)
1199            .map_err(|e| invalid_input_error("source_table_id", e))?;
1200        let target_id = uuid::Uuid::parse_str(target_table_id)
1201            .map_err(|e| invalid_input_error("target_table_id", e))?;
1202
1203        let validator = crate::validation::relationships::RelationshipValidator::new();
1204        match validator.check_circular_dependency(&relationships, source_id, target_id) {
1205            Ok((has_cycle, cycle_path)) => {
1206                let cycle_path_strs: Vec<String> = cycle_path
1207                    .map(|path| path.iter().map(|id| id.to_string()).collect())
1208                    .unwrap_or_default();
1209                Ok(serde_json::json!({
1210                    "has_cycle": has_cycle,
1211                    "cycle_path": cycle_path_strs
1212                })
1213                .to_string())
1214            }
1215            Err(err) => Err(validation_error(err)),
1216        }
1217    }
1218
1219    /// Validate that source and target tables are different (no self-reference).
1220    ///
1221    /// # Arguments
1222    ///
1223    /// * `source_table_id` - Source table ID (UUID string)
1224    /// * `target_table_id` - Target table ID (UUID string)
1225    ///
1226    /// # Returns
1227    ///
1228    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "self_reference": {...}}`
1229    #[wasm_bindgen]
1230    pub fn validate_no_self_reference(
1231        source_table_id: &str,
1232        target_table_id: &str,
1233    ) -> Result<String, JsValue> {
1234        let source_id = uuid::Uuid::parse_str(source_table_id)
1235            .map_err(|e| invalid_input_error("source_table_id", e))?;
1236        let target_id = uuid::Uuid::parse_str(target_table_id)
1237            .map_err(|e| invalid_input_error("target_table_id", e))?;
1238
1239        let validator = crate::validation::relationships::RelationshipValidator::new();
1240        match validator.validate_no_self_reference(source_id, target_id) {
1241            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1242            Err(self_ref) => {
1243                Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1244            }
1245        }
1246    }
1247
1248    // ============================================================================
1249    // PNG Export
1250    // ============================================================================
1251
1252    /// Export a data model to PNG image format.
1253    ///
1254    /// # Arguments
1255    ///
1256    /// * `workspace_json` - JSON string containing workspace/data model structure
1257    /// * `width` - Image width in pixels
1258    /// * `height` - Image height in pixels
1259    ///
1260    /// # Returns
1261    ///
1262    /// Base64-encoded PNG image string, or JsValue error
1263    #[cfg(feature = "png-export")]
1264    #[wasm_bindgen]
1265    pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1266        let model = deserialize_workspace(workspace_json)?;
1267        let exporter = crate::export::PNGExporter::new();
1268        match exporter.export(&model.tables, width, height) {
1269            Ok(result) => Ok(result.content), // Already base64-encoded
1270            Err(err) => Err(export_error_to_js(err)),
1271        }
1272    }
1273
1274    // ============================================================================
1275    // Model Loading/Saving (Async)
1276    // ============================================================================
1277
1278    /// Load a model from browser storage (IndexedDB/localStorage).
1279    ///
1280    /// # Arguments
1281    ///
1282    /// * `db_name` - IndexedDB database name
1283    /// * `store_name` - Object store name
1284    /// * `workspace_path` - Workspace path to load from
1285    ///
1286    /// # Returns
1287    ///
1288    /// Promise that resolves to JSON string containing ModelLoadResult, or rejects with error
1289    #[wasm_bindgen]
1290    pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1291        let db_name = db_name.to_string();
1292        let store_name = store_name.to_string();
1293        let workspace_path = workspace_path.to_string();
1294
1295        wasm_bindgen_futures::future_to_promise(async move {
1296            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1297            let loader = crate::model::ModelLoader::new(storage);
1298            match loader.load_model(&workspace_path).await {
1299                Ok(result) => serde_json::to_string(&result)
1300                    .map(|s| JsValue::from_str(&s))
1301                    .map_err(serialization_error),
1302                Err(err) => Err(storage_error(err)),
1303            }
1304        })
1305    }
1306
1307    /// Save a model to browser storage (IndexedDB/localStorage).
1308    ///
1309    /// # Arguments
1310    ///
1311    /// * `db_name` - IndexedDB database name
1312    /// * `store_name` - Object store name
1313    /// * `workspace_path` - Workspace path to save to
1314    /// * `model_json` - JSON string containing DataModel to save
1315    ///
1316    /// # Returns
1317    ///
1318    /// Promise that resolves to success message, or rejects with error
1319    #[wasm_bindgen]
1320    pub fn save_model(
1321        db_name: &str,
1322        store_name: &str,
1323        workspace_path: &str,
1324        model_json: &str,
1325    ) -> js_sys::Promise {
1326        let db_name = db_name.to_string();
1327        let store_name = store_name.to_string();
1328        let workspace_path = workspace_path.to_string();
1329        let model_json = model_json.to_string();
1330
1331        wasm_bindgen_futures::future_to_promise(async move {
1332            let model: crate::models::DataModel =
1333                serde_json::from_str(&model_json).map_err(deserialization_error)?;
1334
1335            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1336            let saver = crate::model::ModelSaver::new(storage);
1337
1338            // Convert DataModel to table/relationship data for saving
1339            // For each table, save as YAML
1340            for table in &model.tables {
1341                // Export table to ODCS YAML
1342                let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1343                let table_data = crate::model::saver::TableData {
1344                    id: table.id,
1345                    name: table.name.clone(),
1346                    yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1347                    yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1348                };
1349                saver
1350                    .save_table(&workspace_path, &table_data)
1351                    .await
1352                    .map_err(storage_error)?;
1353            }
1354
1355            // Save relationships
1356            if !model.relationships.is_empty() {
1357                let rel_data: Vec<crate::model::saver::RelationshipData> = model
1358                    .relationships
1359                    .iter()
1360                    .map(|rel| {
1361                        let yaml_value = serde_json::json!({
1362                            "id": rel.id.to_string(),
1363                            "source_table_id": rel.source_table_id.to_string(),
1364                            "target_table_id": rel.target_table_id.to_string(),
1365                        });
1366                        // Convert JSON value to YAML value
1367                        let yaml_str = serde_json::to_string(&yaml_value)
1368                            .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1369                        let yaml_value = serde_yaml::from_str(&yaml_str)
1370                            .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1371                        Ok(crate::model::saver::RelationshipData {
1372                            id: rel.id,
1373                            source_table_id: rel.source_table_id,
1374                            target_table_id: rel.target_table_id,
1375                            yaml_value,
1376                        })
1377                    })
1378                    .collect::<Result<Vec<_>, String>>()
1379                    .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1380
1381                saver
1382                    .save_relationships(&workspace_path, &rel_data)
1383                    .await
1384                    .map_err(|e| storage_error(e))?;
1385            }
1386
1387            Ok(JsValue::from_str("Model saved successfully"))
1388        })
1389    }
1390
1391    // BPMN WASM Bindings
1392    /// Import a BPMN model from XML content.
1393    ///
1394    /// # Arguments
1395    ///
1396    /// * `domain_id` - Domain UUID as string
1397    /// * `xml_content` - BPMN XML content as a string
1398    /// * `model_name` - Optional model name (extracted from XML if not provided)
1399    ///
1400    /// # Returns
1401    ///
1402    /// JSON string containing BPMNModel, or JsValue error
1403    #[cfg(feature = "bpmn")]
1404    #[wasm_bindgen]
1405    pub fn import_bpmn_model(
1406        domain_id: &str,
1407        xml_content: &str,
1408        model_name: Option<String>,
1409    ) -> Result<String, JsValue> {
1410        use crate::import::bpmn::BPMNImporter;
1411        use uuid::Uuid;
1412
1413        let domain_uuid =
1414            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1415
1416        let mut importer = BPMNImporter::new();
1417        match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1418            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1419            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1420        }
1421    }
1422
1423    /// Export a BPMN model to XML content.
1424    ///
1425    /// # Arguments
1426    ///
1427    /// * `xml_content` - BPMN XML content as a string
1428    ///
1429    /// # Returns
1430    ///
1431    /// BPMN XML content as string, or JsValue error
1432    #[cfg(feature = "bpmn")]
1433    #[wasm_bindgen]
1434    pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1435        use crate::export::bpmn::BPMNExporter;
1436        let exporter = BPMNExporter::new();
1437        exporter
1438            .export(xml_content)
1439            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1440    }
1441
1442    // DMN WASM Bindings
1443    /// Import a DMN model from XML content.
1444    ///
1445    /// # Arguments
1446    ///
1447    /// * `domain_id` - Domain UUID as string
1448    /// * `xml_content` - DMN XML content as a string
1449    /// * `model_name` - Optional model name (extracted from XML if not provided)
1450    ///
1451    /// # Returns
1452    ///
1453    /// JSON string containing DMNModel, or JsValue error
1454    #[cfg(feature = "dmn")]
1455    #[wasm_bindgen]
1456    pub fn import_dmn_model(
1457        domain_id: &str,
1458        xml_content: &str,
1459        model_name: Option<String>,
1460    ) -> Result<String, JsValue> {
1461        use crate::import::dmn::DMNImporter;
1462        use uuid::Uuid;
1463
1464        let domain_uuid =
1465            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1466
1467        let mut importer = DMNImporter::new();
1468        match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1469            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1470            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1471        }
1472    }
1473
1474    /// Export a DMN model to XML content.
1475    ///
1476    /// # Arguments
1477    ///
1478    /// * `xml_content` - DMN XML content as a string
1479    ///
1480    /// # Returns
1481    ///
1482    /// DMN XML content as string, or JsValue error
1483    #[cfg(feature = "dmn")]
1484    #[wasm_bindgen]
1485    pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1486        use crate::export::dmn::DMNExporter;
1487        let exporter = DMNExporter::new();
1488        exporter
1489            .export(xml_content)
1490            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1491    }
1492
1493    // OpenAPI WASM Bindings
1494    /// Import an OpenAPI specification from YAML or JSON content.
1495    ///
1496    /// # Arguments
1497    ///
1498    /// * `domain_id` - Domain UUID as string
1499    /// * `content` - OpenAPI YAML or JSON content as a string
1500    /// * `api_name` - Optional API name (extracted from info.title if not provided)
1501    ///
1502    /// # Returns
1503    ///
1504    /// JSON string containing OpenAPIModel, or JsValue error
1505    #[cfg(feature = "openapi")]
1506    #[wasm_bindgen]
1507    pub fn import_openapi_spec(
1508        domain_id: &str,
1509        content: &str,
1510        api_name: Option<String>,
1511    ) -> Result<String, JsValue> {
1512        use crate::import::openapi::OpenAPIImporter;
1513        use uuid::Uuid;
1514
1515        let domain_uuid =
1516            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1517
1518        let mut importer = OpenAPIImporter::new();
1519        match importer.import(content, domain_uuid, api_name.as_deref()) {
1520            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1521            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1522        }
1523    }
1524
1525    /// Export an OpenAPI specification to YAML or JSON content.
1526    ///
1527    /// # Arguments
1528    ///
1529    /// * `content` - OpenAPI content as a string
1530    /// * `source_format` - Source format ("yaml" or "json")
1531    /// * `target_format` - Optional target format for conversion (None to keep original)
1532    ///
1533    /// # Returns
1534    ///
1535    /// OpenAPI content in requested format, or JsValue error
1536    #[cfg(feature = "openapi")]
1537    #[wasm_bindgen]
1538    pub fn export_openapi_spec(
1539        content: &str,
1540        source_format: &str,
1541        target_format: Option<String>,
1542    ) -> Result<String, JsValue> {
1543        use crate::export::openapi::OpenAPIExporter;
1544        use crate::models::openapi::OpenAPIFormat;
1545
1546        let source_fmt = match source_format {
1547            "yaml" | "yml" => OpenAPIFormat::Yaml,
1548            "json" => OpenAPIFormat::Json,
1549            _ => {
1550                return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1551            }
1552        };
1553
1554        let target_fmt = if let Some(tf) = target_format {
1555            match tf.as_str() {
1556                "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1557                "json" => Some(OpenAPIFormat::Json),
1558                _ => {
1559                    return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1560                }
1561            }
1562        } else {
1563            None
1564        };
1565
1566        let exporter = OpenAPIExporter::new();
1567        exporter
1568            .export(content, source_fmt, target_fmt)
1569            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1570    }
1571
1572    /// Convert an OpenAPI schema component to an ODCS table.
1573    ///
1574    /// # Arguments
1575    ///
1576    /// * `openapi_content` - OpenAPI YAML or JSON content as a string
1577    /// * `component_name` - Name of the schema component to convert
1578    /// * `table_name` - Optional desired ODCS table name (uses component_name if None)
1579    ///
1580    /// # Returns
1581    ///
1582    /// JSON string containing ODCS Table, or JsValue error
1583    #[cfg(feature = "openapi")]
1584    #[wasm_bindgen]
1585    pub fn convert_openapi_to_odcs(
1586        openapi_content: &str,
1587        component_name: &str,
1588        table_name: Option<String>,
1589    ) -> Result<String, JsValue> {
1590        use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1591
1592        let converter = OpenAPIToODCSConverter::new();
1593        match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1594            Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1595            Err(e) => Err(conversion_error(e)),
1596        }
1597    }
1598
1599    /// Analyze an OpenAPI component for conversion feasibility.
1600    ///
1601    /// # Arguments
1602    ///
1603    /// * `openapi_content` - OpenAPI YAML or JSON content as a string
1604    /// * `component_name` - Name of the schema component to analyze
1605    ///
1606    /// # Returns
1607    ///
1608    /// JSON string containing ConversionReport, or JsValue error
1609    #[cfg(feature = "openapi")]
1610    #[wasm_bindgen]
1611    pub fn analyze_openapi_conversion(
1612        openapi_content: &str,
1613        component_name: &str,
1614    ) -> Result<String, JsValue> {
1615        use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1616
1617        let converter = OpenAPIToODCSConverter::new();
1618        match converter.analyze_conversion(openapi_content, component_name) {
1619            Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1620            Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1621                .with_code("ANALYSIS_FAILED")
1622                .to_js_value()),
1623        }
1624    }
1625
1626    // ============================================================================
1627    // Workspace and DomainConfig Operations
1628    // ============================================================================
1629
1630    /// Create a new workspace.
1631    ///
1632    /// # Arguments
1633    ///
1634    /// * `name` - Workspace name
1635    /// * `owner_id` - Owner UUID as string
1636    ///
1637    /// # Returns
1638    ///
1639    /// JSON string containing Workspace, or JsValue error
1640    #[wasm_bindgen]
1641    pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1642        use crate::models::workspace::Workspace;
1643        use chrono::Utc;
1644        use uuid::Uuid;
1645
1646        let owner_uuid =
1647            Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1648
1649        let workspace = Workspace::new(name.to_string(), owner_uuid);
1650
1651        serde_json::to_string(&workspace).map_err(serialization_error)
1652    }
1653
1654    /// Parse workspace YAML content and return a structured representation.
1655    ///
1656    /// # Arguments
1657    ///
1658    /// * `yaml_content` - Workspace YAML content as a string
1659    ///
1660    /// # Returns
1661    ///
1662    /// JSON string containing Workspace, or JsValue error
1663    #[wasm_bindgen]
1664    pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1665        use crate::models::workspace::Workspace;
1666
1667        let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1668        serde_json::to_string(&workspace).map_err(serialization_error)
1669    }
1670
1671    /// Export a workspace to YAML format.
1672    ///
1673    /// # Arguments
1674    ///
1675    /// * `workspace_json` - JSON string containing Workspace
1676    ///
1677    /// # Returns
1678    ///
1679    /// Workspace YAML format string, or JsValue error
1680    #[wasm_bindgen]
1681    pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1682        use crate::models::workspace::Workspace;
1683
1684        let workspace: Workspace =
1685            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1686        serde_yaml::to_string(&workspace).map_err(serialization_error)
1687    }
1688
1689    /// Add a domain reference to a workspace.
1690    ///
1691    /// # Arguments
1692    ///
1693    /// * `workspace_json` - JSON string containing Workspace
1694    /// * `domain_id` - Domain UUID as string
1695    /// * `domain_name` - Domain name
1696    ///
1697    /// # Returns
1698    ///
1699    /// JSON string containing updated Workspace, or JsValue error
1700    #[wasm_bindgen]
1701    pub fn add_domain_to_workspace(
1702        workspace_json: &str,
1703        domain_id: &str,
1704        domain_name: &str,
1705    ) -> Result<String, JsValue> {
1706        use crate::models::workspace::{DomainReference, Workspace};
1707        use chrono::Utc;
1708        use uuid::Uuid;
1709
1710        let mut workspace: Workspace =
1711            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1712        let domain_uuid =
1713            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1714
1715        // Check if domain already exists
1716        if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1717            return Err(WasmError::new(
1718                "DuplicateError",
1719                format!("Domain {} already exists in workspace", domain_id),
1720            )
1721            .with_code("DUPLICATE_DOMAIN")
1722            .to_js_value());
1723        }
1724
1725        workspace.domains.push(DomainReference {
1726            id: domain_uuid,
1727            name: domain_name.to_string(),
1728            description: None,
1729            systems: Vec::new(),
1730        });
1731        workspace.last_modified_at = Utc::now();
1732
1733        serde_json::to_string(&workspace).map_err(serialization_error)
1734    }
1735
1736    /// Remove a domain reference from a workspace.
1737    ///
1738    /// # Arguments
1739    ///
1740    /// * `workspace_json` - JSON string containing Workspace
1741    /// * `domain_id` - Domain UUID as string to remove
1742    ///
1743    /// # Returns
1744    ///
1745    /// JSON string containing updated Workspace, or JsValue error
1746    #[wasm_bindgen]
1747    pub fn remove_domain_from_workspace(
1748        workspace_json: &str,
1749        domain_id: &str,
1750    ) -> Result<String, JsValue> {
1751        use crate::models::workspace::Workspace;
1752        use chrono::Utc;
1753        use uuid::Uuid;
1754
1755        let mut workspace: Workspace =
1756            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1757        let domain_uuid =
1758            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1759
1760        let original_len = workspace.domains.len();
1761        workspace.domains.retain(|d| d.id != domain_uuid);
1762
1763        if workspace.domains.len() == original_len {
1764            return Err(WasmError::new(
1765                "NotFoundError",
1766                format!("Domain {} not found in workspace", domain_id),
1767            )
1768            .with_code("DOMAIN_NOT_FOUND")
1769            .to_js_value());
1770        }
1771
1772        workspace.last_modified_at = Utc::now();
1773        serde_json::to_string(&workspace).map_err(serialization_error)
1774    }
1775
1776    /// Add a relationship to a workspace.
1777    ///
1778    /// # Arguments
1779    ///
1780    /// * `workspace_json` - JSON string containing Workspace
1781    /// * `relationship_json` - JSON string containing Relationship
1782    ///
1783    /// # Returns
1784    ///
1785    /// JSON string containing updated Workspace, or JsValue error
1786    #[wasm_bindgen]
1787    pub fn add_relationship_to_workspace(
1788        workspace_json: &str,
1789        relationship_json: &str,
1790    ) -> Result<String, JsValue> {
1791        use crate::models::Relationship;
1792        use crate::models::workspace::Workspace;
1793
1794        let mut workspace: Workspace =
1795            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1796        let relationship: Relationship =
1797            serde_json::from_str(relationship_json).map_err(deserialization_error)?;
1798
1799        // Check if relationship already exists
1800        if workspace
1801            .relationships
1802            .iter()
1803            .any(|r| r.id == relationship.id)
1804        {
1805            return Err(WasmError::new(
1806                "DuplicateError",
1807                format!(
1808                    "Relationship {} already exists in workspace",
1809                    relationship.id
1810                ),
1811            )
1812            .with_code("DUPLICATE_RELATIONSHIP")
1813            .to_js_value());
1814        }
1815
1816        workspace.add_relationship(relationship);
1817        serde_json::to_string(&workspace).map_err(serialization_error)
1818    }
1819
1820    /// Remove a relationship from a workspace.
1821    ///
1822    /// # Arguments
1823    ///
1824    /// * `workspace_json` - JSON string containing Workspace
1825    /// * `relationship_id` - Relationship UUID as string to remove
1826    ///
1827    /// # Returns
1828    ///
1829    /// JSON string containing updated Workspace, or JsValue error
1830    #[wasm_bindgen]
1831    pub fn remove_relationship_from_workspace(
1832        workspace_json: &str,
1833        relationship_id: &str,
1834    ) -> Result<String, JsValue> {
1835        use crate::models::workspace::Workspace;
1836        use uuid::Uuid;
1837
1838        let mut workspace: Workspace =
1839            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1840        let relationship_uuid = Uuid::parse_str(relationship_id)
1841            .map_err(|e| invalid_input_error("relationship ID", e))?;
1842
1843        if !workspace.remove_relationship(relationship_uuid) {
1844            return Err(WasmError::new(
1845                "NotFoundError",
1846                format!("Relationship {} not found in workspace", relationship_id),
1847            )
1848            .with_code("RELATIONSHIP_NOT_FOUND")
1849            .to_js_value());
1850        }
1851
1852        serde_json::to_string(&workspace).map_err(serialization_error)
1853    }
1854
1855    /// Get relationships for a source table from a workspace.
1856    ///
1857    /// # Arguments
1858    ///
1859    /// * `workspace_json` - JSON string containing Workspace
1860    /// * `source_table_id` - Source table UUID as string
1861    ///
1862    /// # Returns
1863    ///
1864    /// JSON string containing array of Relationships, or JsValue error
1865    #[wasm_bindgen]
1866    pub fn get_workspace_relationships_for_source(
1867        workspace_json: &str,
1868        source_table_id: &str,
1869    ) -> Result<String, JsValue> {
1870        use crate::models::workspace::Workspace;
1871        use uuid::Uuid;
1872
1873        let workspace: Workspace =
1874            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1875        let source_uuid = Uuid::parse_str(source_table_id)
1876            .map_err(|e| invalid_input_error("source table ID", e))?;
1877
1878        let relationships: Vec<_> = workspace.get_relationships_for_source(source_uuid);
1879        serde_json::to_string(&relationships).map_err(serialization_error)
1880    }
1881
1882    /// Get relationships for a target table from a workspace.
1883    ///
1884    /// # Arguments
1885    ///
1886    /// * `workspace_json` - JSON string containing Workspace
1887    /// * `target_table_id` - Target table UUID as string
1888    ///
1889    /// # Returns
1890    ///
1891    /// JSON string containing array of Relationships, or JsValue error
1892    #[wasm_bindgen]
1893    pub fn get_workspace_relationships_for_target(
1894        workspace_json: &str,
1895        target_table_id: &str,
1896    ) -> Result<String, JsValue> {
1897        use crate::models::workspace::Workspace;
1898        use uuid::Uuid;
1899
1900        let workspace: Workspace =
1901            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1902        let target_uuid = Uuid::parse_str(target_table_id)
1903            .map_err(|e| invalid_input_error("target table ID", e))?;
1904
1905        let relationships: Vec<_> = workspace.get_relationships_for_target(target_uuid);
1906        serde_json::to_string(&relationships).map_err(serialization_error)
1907    }
1908
1909    /// Create a new domain configuration.
1910    ///
1911    /// # Arguments
1912    ///
1913    /// * `name` - Domain name
1914    /// * `workspace_id` - Workspace UUID as string
1915    ///
1916    /// # Returns
1917    ///
1918    /// JSON string containing DomainConfig, or JsValue error
1919    #[wasm_bindgen]
1920    pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1921        use crate::models::domain_config::DomainConfig;
1922        use chrono::Utc;
1923        use std::collections::HashMap;
1924        use uuid::Uuid;
1925
1926        let workspace_uuid =
1927            Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1928
1929        let config = DomainConfig {
1930            id: Uuid::new_v4(),
1931            workspace_id: workspace_uuid,
1932            name: name.to_string(),
1933            description: None,
1934            created_at: Utc::now(),
1935            last_modified_at: Utc::now(),
1936            owner: None,
1937            systems: Vec::new(),
1938            tables: Vec::new(),
1939            products: Vec::new(),
1940            assets: Vec::new(),
1941            processes: Vec::new(),
1942            decisions: Vec::new(),
1943            view_positions: HashMap::new(),
1944            folder_path: None,
1945            workspace_path: None,
1946        };
1947
1948        serde_json::to_string(&config).map_err(serialization_error)
1949    }
1950
1951    /// Parse domain config YAML content and return a structured representation.
1952    ///
1953    /// # Arguments
1954    ///
1955    /// * `yaml_content` - Domain config YAML content as a string
1956    ///
1957    /// # Returns
1958    ///
1959    /// JSON string containing DomainConfig, or JsValue error
1960    #[wasm_bindgen]
1961    pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1962        use crate::models::domain_config::DomainConfig;
1963
1964        let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1965        serde_json::to_string(&config).map_err(serialization_error)
1966    }
1967
1968    /// Export a domain config to YAML format.
1969    ///
1970    /// # Arguments
1971    ///
1972    /// * `config_json` - JSON string containing DomainConfig
1973    ///
1974    /// # Returns
1975    ///
1976    /// DomainConfig YAML format string, or JsValue error
1977    #[wasm_bindgen]
1978    pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1979        use crate::models::domain_config::DomainConfig;
1980
1981        let config: DomainConfig =
1982            serde_json::from_str(config_json).map_err(deserialization_error)?;
1983        serde_yaml::to_string(&config).map_err(serialization_error)
1984    }
1985
1986    /// Get the domain ID from a domain config JSON.
1987    ///
1988    /// # Arguments
1989    ///
1990    /// * `config_json` - JSON string containing DomainConfig
1991    ///
1992    /// # Returns
1993    ///
1994    /// Domain UUID as string, or JsValue error
1995    #[wasm_bindgen]
1996    pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1997        use crate::models::domain_config::DomainConfig;
1998
1999        let config: DomainConfig =
2000            serde_json::from_str(config_json).map_err(deserialization_error)?;
2001        Ok(config.id.to_string())
2002    }
2003
2004    /// Update domain config with new view positions.
2005    ///
2006    /// # Arguments
2007    ///
2008    /// * `config_json` - JSON string containing DomainConfig
2009    /// * `positions_json` - JSON string containing view positions map
2010    ///
2011    /// # Returns
2012    ///
2013    /// JSON string containing updated DomainConfig, or JsValue error
2014    #[wasm_bindgen]
2015    pub fn update_domain_view_positions(
2016        config_json: &str,
2017        positions_json: &str,
2018    ) -> Result<String, JsValue> {
2019        use crate::models::domain_config::{DomainConfig, ViewPosition};
2020        use chrono::Utc;
2021        use std::collections::HashMap;
2022
2023        let mut config: DomainConfig =
2024            serde_json::from_str(config_json).map_err(deserialization_error)?;
2025        let positions: HashMap<String, HashMap<String, ViewPosition>> =
2026            serde_json::from_str(positions_json).map_err(deserialization_error)?;
2027
2028        config.view_positions = positions;
2029        config.last_modified_at = Utc::now();
2030
2031        serde_json::to_string(&config).map_err(serialization_error)
2032    }
2033
2034    /// Add an entity reference to a domain config.
2035    ///
2036    /// # Arguments
2037    ///
2038    /// * `config_json` - JSON string containing DomainConfig
2039    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2040    /// * `entity_id` - Entity UUID as string
2041    ///
2042    /// # Returns
2043    ///
2044    /// JSON string containing updated DomainConfig, or JsValue error
2045    #[wasm_bindgen]
2046    pub fn add_entity_to_domain_config(
2047        config_json: &str,
2048        entity_type: &str,
2049        entity_id: &str,
2050    ) -> Result<String, JsValue> {
2051        use crate::models::domain_config::DomainConfig;
2052        use chrono::Utc;
2053        use uuid::Uuid;
2054
2055        let mut config: DomainConfig =
2056            serde_json::from_str(config_json).map_err(deserialization_error)?;
2057        let entity_uuid =
2058            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2059
2060        let entities = match entity_type {
2061            "system" => &mut config.systems,
2062            "table" => &mut config.tables,
2063            "product" => &mut config.products,
2064            "asset" => &mut config.assets,
2065            "process" => &mut config.processes,
2066            "decision" => &mut config.decisions,
2067            _ => {
2068                return Err(invalid_input_error(
2069                    "entity type",
2070                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2071                ));
2072            }
2073        };
2074
2075        if entities.contains(&entity_uuid) {
2076            return Err(WasmError::new(
2077                "DuplicateError",
2078                format!(
2079                    "{} {} already exists in domain config",
2080                    entity_type, entity_id
2081                ),
2082            )
2083            .with_code("DUPLICATE_ENTITY")
2084            .to_js_value());
2085        }
2086
2087        entities.push(entity_uuid);
2088        config.last_modified_at = Utc::now();
2089
2090        serde_json::to_string(&config).map_err(serialization_error)
2091    }
2092
2093    /// Remove an entity reference from a domain config.
2094    ///
2095    /// # Arguments
2096    ///
2097    /// * `config_json` - JSON string containing DomainConfig
2098    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2099    /// * `entity_id` - Entity UUID as string to remove
2100    ///
2101    /// # Returns
2102    ///
2103    /// JSON string containing updated DomainConfig, or JsValue error
2104    #[wasm_bindgen]
2105    pub fn remove_entity_from_domain_config(
2106        config_json: &str,
2107        entity_type: &str,
2108        entity_id: &str,
2109    ) -> Result<String, JsValue> {
2110        use crate::models::domain_config::DomainConfig;
2111        use chrono::Utc;
2112        use uuid::Uuid;
2113
2114        let mut config: DomainConfig =
2115            serde_json::from_str(config_json).map_err(deserialization_error)?;
2116        let entity_uuid =
2117            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2118
2119        let entities = match entity_type {
2120            "system" => &mut config.systems,
2121            "table" => &mut config.tables,
2122            "product" => &mut config.products,
2123            "asset" => &mut config.assets,
2124            "process" => &mut config.processes,
2125            "decision" => &mut config.decisions,
2126            _ => {
2127                return Err(invalid_input_error(
2128                    "entity type",
2129                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2130                ));
2131            }
2132        };
2133
2134        let original_len = entities.len();
2135        entities.retain(|id| *id != entity_uuid);
2136
2137        if entities.len() == original_len {
2138            return Err(WasmError::new(
2139                "NotFoundError",
2140                format!("{} {} not found in domain config", entity_type, entity_id),
2141            )
2142            .with_code("ENTITY_NOT_FOUND")
2143            .to_js_value());
2144        }
2145
2146        config.last_modified_at = Utc::now();
2147        serde_json::to_string(&config).map_err(serialization_error)
2148    }
2149}