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        use crate::validation::schema::validate_odps_internal;
643        validate_odps_internal(yaml_content).map_err(validation_error)
644    }
645
646    #[cfg(not(feature = "odps-validation"))]
647    #[wasm_bindgen]
648    pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
649        // Validation disabled - feature not enabled
650        // Return success to maintain backward compatibility
651        Ok(())
652    }
653
654    /// Create a new business domain.
655    ///
656    /// # Arguments
657    ///
658    /// * `name` - Domain name
659    ///
660    /// # Returns
661    ///
662    /// JSON string containing Domain, or JsValue error
663    #[wasm_bindgen]
664    pub fn create_domain(name: &str) -> Result<String, JsValue> {
665        let domain = crate::models::domain::Domain::new(name.to_string());
666        serde_json::to_string(&domain).map_err(serialization_error)
667    }
668
669    /// Import Domain YAML content and return a structured representation.
670    ///
671    /// # Arguments
672    ///
673    /// * `yaml_content` - Domain YAML content as a string
674    ///
675    /// # Returns
676    ///
677    /// JSON string containing Domain, or JsValue error
678    #[wasm_bindgen]
679    pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
680        match crate::models::domain::Domain::from_yaml(yaml_content) {
681            Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
682            Err(e) => Err(parse_error(e)),
683        }
684    }
685
686    /// Export a Domain to YAML format.
687    ///
688    /// # Arguments
689    ///
690    /// * `domain_json` - JSON string containing Domain
691    ///
692    /// # Returns
693    ///
694    /// Domain YAML format string, or JsValue error
695    #[wasm_bindgen]
696    pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
697        let domain: crate::models::domain::Domain =
698            serde_json::from_str(domain_json).map_err(deserialization_error)?;
699        domain.to_yaml().map_err(serialization_error)
700    }
701
702    /// Migrate DataFlow YAML to Domain schema format.
703    ///
704    /// # Arguments
705    ///
706    /// * `dataflow_yaml` - DataFlow YAML content as a string
707    /// * `domain_name` - Optional domain name (defaults to "MigratedDomain")
708    ///
709    /// # Returns
710    ///
711    /// JSON string containing Domain, or JsValue error
712    #[wasm_bindgen]
713    pub fn migrate_dataflow_to_domain(
714        dataflow_yaml: &str,
715        domain_name: Option<String>,
716    ) -> Result<String, JsValue> {
717        match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
718            dataflow_yaml,
719            domain_name.as_deref(),
720        ) {
721            Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
722            Err(e) => Err(conversion_error(e)),
723        }
724    }
725
726    /// Parse a tag string into a Tag enum.
727    ///
728    /// # Arguments
729    ///
730    /// * `tag_str` - Tag string (Simple, Pair, or List format)
731    ///
732    /// # Returns
733    ///
734    /// JSON string containing Tag, or JsValue error
735    #[wasm_bindgen]
736    pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
737        use crate::models::Tag;
738        use std::str::FromStr;
739        match Tag::from_str(tag_str) {
740            Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
741            Err(_) => Err(parse_error("Invalid tag format")),
742        }
743    }
744
745    /// Serialize a Tag enum to string format.
746    ///
747    /// # Arguments
748    ///
749    /// * `tag_json` - JSON string containing Tag
750    ///
751    /// # Returns
752    ///
753    /// Tag string (Simple, Pair, or List format), or JsValue error
754    #[wasm_bindgen]
755    pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
756        use crate::models::Tag;
757        let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
758        Ok(tag.to_string())
759    }
760
761    /// Convert any format to ODCS v3.1.0 YAML format.
762    ///
763    /// # Arguments
764    ///
765    /// * `input` - Format-specific content as a string
766    /// * `format` - Optional format identifier. If None, attempts auto-detection.
767    ///   Supported formats: "sql", "json_schema", "avro", "protobuf", "odcl", "odcs", "cads", "odps", "domain"
768    ///
769    /// # Returns
770    ///
771    /// ODCS v3.1.0 YAML string, or JsValue error
772    #[wasm_bindgen]
773    pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
774        match crate::convert::convert_to_odcs(input, format.as_deref()) {
775            Ok(yaml) => Ok(yaml),
776            Err(e) => Err(conversion_error(e)),
777        }
778    }
779
780    /// Filter Data Flow nodes (tables) by owner.
781    ///
782    /// # Arguments
783    ///
784    /// * `workspace_json` - JSON string containing workspace/data model structure
785    /// * `owner` - Owner name to filter by (case-sensitive exact match)
786    ///
787    /// # Returns
788    ///
789    /// JSON string containing array of matching tables, or JsValue error
790    #[wasm_bindgen]
791    pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
792        let model = deserialize_workspace(workspace_json)?;
793        let filtered = model.filter_nodes_by_owner(owner);
794        serde_json::to_string(&filtered).map_err(serialization_error)
795    }
796
797    /// Filter Data Flow relationships by owner.
798    ///
799    /// # Arguments
800    ///
801    /// * `workspace_json` - JSON string containing workspace/data model structure
802    /// * `owner` - Owner name to filter by (case-sensitive exact match)
803    ///
804    /// # Returns
805    ///
806    /// JSON string containing array of matching relationships, or JsValue error
807    #[wasm_bindgen]
808    pub fn filter_relationships_by_owner(
809        workspace_json: &str,
810        owner: &str,
811    ) -> Result<String, JsValue> {
812        let model = deserialize_workspace(workspace_json)?;
813        let filtered = model.filter_relationships_by_owner(owner);
814        serde_json::to_string(&filtered).map_err(serialization_error)
815    }
816
817    /// Filter Data Flow nodes (tables) by infrastructure type.
818    ///
819    /// # Arguments
820    ///
821    /// * `workspace_json` - JSON string containing workspace/data model structure
822    /// * `infrastructure_type` - Infrastructure type string (e.g., "Kafka", "PostgreSQL")
823    ///
824    /// # Returns
825    ///
826    /// JSON string containing array of matching tables, or JsValue error
827    #[wasm_bindgen]
828    pub fn filter_nodes_by_infrastructure_type(
829        workspace_json: &str,
830        infrastructure_type: &str,
831    ) -> Result<String, JsValue> {
832        let model = deserialize_workspace(workspace_json)?;
833        let infra_type: crate::models::enums::InfrastructureType =
834            serde_json::from_str(&format!("\"{}\"", infrastructure_type))
835                .map_err(|e| invalid_input_error("infrastructure type", e))?;
836        let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
837        serde_json::to_string(&filtered).map_err(serialization_error)
838    }
839
840    /// Filter Data Flow relationships by infrastructure type.
841    ///
842    /// # Arguments
843    ///
844    /// * `workspace_json` - JSON string containing workspace/data model structure
845    /// * `infrastructure_type` - Infrastructure type string (e.g., "Kafka", "PostgreSQL")
846    ///
847    /// # Returns
848    ///
849    /// JSON string containing array of matching relationships, or JsValue error
850    #[wasm_bindgen]
851    pub fn filter_relationships_by_infrastructure_type(
852        workspace_json: &str,
853        infrastructure_type: &str,
854    ) -> Result<String, JsValue> {
855        let model = deserialize_workspace(workspace_json)?;
856        let infra_type: crate::models::enums::InfrastructureType =
857            serde_json::from_str(&format!("\"{}\"", infrastructure_type))
858                .map_err(|e| invalid_input_error("infrastructure type", e))?;
859        let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
860        serde_json::to_string(&filtered).map_err(serialization_error)
861    }
862
863    /// Filter Data Flow nodes and relationships by tag.
864    ///
865    /// # Arguments
866    ///
867    /// * `workspace_json` - JSON string containing workspace/data model structure
868    /// * `tag` - Tag to filter by
869    ///
870    /// # Returns
871    ///
872    /// JSON string containing object with `nodes` and `relationships` arrays, or JsValue error
873    #[wasm_bindgen]
874    pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
875        let model = deserialize_workspace(workspace_json)?;
876        let (nodes, relationships) = model.filter_by_tags(tag);
877        let result = serde_json::json!({
878            "nodes": nodes,
879            "relationships": relationships
880        });
881        serde_json::to_string(&result).map_err(serialization_error)
882    }
883
884    // ============================================================================
885    // Domain Operations
886    // ============================================================================
887
888    /// Add a system to a domain in a DataModel.
889    ///
890    /// # Arguments
891    ///
892    /// * `workspace_json` - JSON string containing workspace/data model structure
893    /// * `domain_id` - Domain UUID as string
894    /// * `system_json` - JSON string containing System
895    ///
896    /// # Returns
897    ///
898    /// JSON string containing updated DataModel, or JsValue error
899    #[wasm_bindgen]
900    pub fn add_system_to_domain(
901        workspace_json: &str,
902        domain_id: &str,
903        system_json: &str,
904    ) -> Result<String, JsValue> {
905        let mut model = deserialize_workspace(workspace_json)?;
906        let domain_uuid =
907            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
908        let system: crate::models::domain::System =
909            serde_json::from_str(system_json).map_err(deserialization_error)?;
910        model
911            .add_system_to_domain(domain_uuid, system)
912            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
913        serde_json::to_string(&model).map_err(serialization_error)
914    }
915
916    /// Add a CADS node to a domain in a DataModel.
917    ///
918    /// # Arguments
919    ///
920    /// * `workspace_json` - JSON string containing workspace/data model structure
921    /// * `domain_id` - Domain UUID as string
922    /// * `node_json` - JSON string containing CADSNode
923    ///
924    /// # Returns
925    ///
926    /// JSON string containing updated DataModel, or JsValue error
927    #[wasm_bindgen]
928    pub fn add_cads_node_to_domain(
929        workspace_json: &str,
930        domain_id: &str,
931        node_json: &str,
932    ) -> Result<String, JsValue> {
933        let mut model = deserialize_workspace(workspace_json)?;
934        let domain_uuid =
935            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
936        let node: crate::models::domain::CADSNode =
937            serde_json::from_str(node_json).map_err(deserialization_error)?;
938        model
939            .add_cads_node_to_domain(domain_uuid, node)
940            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
941        serde_json::to_string(&model).map_err(serialization_error)
942    }
943
944    /// Add an ODCS node to a domain in a DataModel.
945    ///
946    /// # Arguments
947    ///
948    /// * `workspace_json` - JSON string containing workspace/data model structure
949    /// * `domain_id` - Domain UUID as string
950    /// * `node_json` - JSON string containing ODCSNode
951    ///
952    /// # Returns
953    ///
954    /// JSON string containing updated DataModel, or JsValue error
955    #[wasm_bindgen]
956    pub fn add_odcs_node_to_domain(
957        workspace_json: &str,
958        domain_id: &str,
959        node_json: &str,
960    ) -> Result<String, JsValue> {
961        let mut model = deserialize_workspace(workspace_json)?;
962        let domain_uuid =
963            uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
964        let node: crate::models::domain::ODCSNode =
965            serde_json::from_str(node_json).map_err(deserialization_error)?;
966        model
967            .add_odcs_node_to_domain(domain_uuid, node)
968            .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
969        serde_json::to_string(&model).map_err(serialization_error)
970    }
971
972    // ============================================================================
973    // Validation Functions
974    // ============================================================================
975
976    /// Validate a table name.
977    ///
978    /// # Arguments
979    ///
980    /// * `name` - Table name to validate
981    ///
982    /// # Returns
983    ///
984    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
985    #[wasm_bindgen]
986    pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
987        match crate::validation::input::validate_table_name(name) {
988            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
989            Err(err) => {
990                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
991            }
992        }
993    }
994
995    /// Validate a column name.
996    ///
997    /// # Arguments
998    ///
999    /// * `name` - Column name to validate
1000    ///
1001    /// # Returns
1002    ///
1003    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1004    #[wasm_bindgen]
1005    pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
1006        match crate::validation::input::validate_column_name(name) {
1007            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1008            Err(err) => {
1009                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1010            }
1011        }
1012    }
1013
1014    /// Validate a UUID string.
1015    ///
1016    /// # Arguments
1017    ///
1018    /// * `id` - UUID string to validate
1019    ///
1020    /// # Returns
1021    ///
1022    /// JSON string with validation result: `{"valid": true, "uuid": "..."}` or `{"valid": false, "error": "error message"}`
1023    #[wasm_bindgen]
1024    pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
1025        match crate::validation::input::validate_uuid(id) {
1026            Ok(uuid) => {
1027                Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
1028            }
1029            Err(err) => {
1030                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1031            }
1032        }
1033    }
1034
1035    /// Validate a data type string.
1036    ///
1037    /// # Arguments
1038    ///
1039    /// * `data_type` - Data type string to validate
1040    ///
1041    /// # Returns
1042    ///
1043    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1044    #[wasm_bindgen]
1045    pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
1046        match crate::validation::input::validate_data_type(data_type) {
1047            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1048            Err(err) => {
1049                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1050            }
1051        }
1052    }
1053
1054    /// Validate a description string.
1055    ///
1056    /// # Arguments
1057    ///
1058    /// * `desc` - Description string to validate
1059    ///
1060    /// # Returns
1061    ///
1062    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "error": "error message"}`
1063    #[wasm_bindgen]
1064    pub fn validate_description(desc: &str) -> Result<String, JsValue> {
1065        match crate::validation::input::validate_description(desc) {
1066            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1067            Err(err) => {
1068                Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1069            }
1070        }
1071    }
1072
1073    /// Sanitize a SQL identifier by quoting it.
1074    ///
1075    /// # Arguments
1076    ///
1077    /// * `name` - SQL identifier to sanitize
1078    /// * `dialect` - SQL dialect ("postgresql", "mysql", "sqlserver", etc.)
1079    ///
1080    /// # Returns
1081    ///
1082    /// Sanitized SQL identifier string
1083    #[wasm_bindgen]
1084    pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
1085        crate::validation::input::sanitize_sql_identifier(name, dialect)
1086    }
1087
1088    /// Sanitize a description string.
1089    ///
1090    /// # Arguments
1091    ///
1092    /// * `desc` - Description string to sanitize
1093    ///
1094    /// # Returns
1095    ///
1096    /// Sanitized description string
1097    #[wasm_bindgen]
1098    pub fn sanitize_description(desc: &str) -> String {
1099        crate::validation::input::sanitize_description(desc)
1100    }
1101
1102    /// Detect naming conflicts between existing and new tables.
1103    ///
1104    /// # Arguments
1105    ///
1106    /// * `existing_tables_json` - JSON string containing array of existing tables
1107    /// * `new_tables_json` - JSON string containing array of new tables
1108    ///
1109    /// # Returns
1110    ///
1111    /// JSON string containing array of naming conflicts
1112    #[wasm_bindgen]
1113    pub fn detect_naming_conflicts(
1114        existing_tables_json: &str,
1115        new_tables_json: &str,
1116    ) -> Result<String, JsValue> {
1117        let existing_tables: Vec<crate::models::Table> =
1118            serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1119        let new_tables: Vec<crate::models::Table> =
1120            serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1121
1122        let validator = crate::validation::tables::TableValidator::new();
1123        let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1124
1125        serde_json::to_string(&conflicts).map_err(serialization_error)
1126    }
1127
1128    /// Validate pattern exclusivity for a table (SCD pattern and Data Vault classification are mutually exclusive).
1129    ///
1130    /// # Arguments
1131    ///
1132    /// * `table_json` - JSON string containing table to validate
1133    ///
1134    /// # Returns
1135    ///
1136    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "violation": {...}}`
1137    #[wasm_bindgen]
1138    pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1139        let table: crate::models::Table =
1140            serde_json::from_str(table_json).map_err(deserialization_error)?;
1141
1142        let validator = crate::validation::tables::TableValidator::new();
1143        match validator.validate_pattern_exclusivity(&table) {
1144            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1145            Err(violation) => {
1146                Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1147            }
1148        }
1149    }
1150
1151    /// Check for circular dependencies in relationships.
1152    ///
1153    /// # Arguments
1154    ///
1155    /// * `relationships_json` - JSON string containing array of existing relationships
1156    /// * `source_table_id` - Source table ID (UUID string) of the new relationship
1157    /// * `target_table_id` - Target table ID (UUID string) of the new relationship
1158    ///
1159    /// # Returns
1160    ///
1161    /// JSON string with result: `{"has_cycle": true/false, "cycle_path": [...]}` or error
1162    #[wasm_bindgen]
1163    pub fn check_circular_dependency(
1164        relationships_json: &str,
1165        source_table_id: &str,
1166        target_table_id: &str,
1167    ) -> Result<String, JsValue> {
1168        let relationships: Vec<crate::models::Relationship> =
1169            serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1170
1171        let source_id = uuid::Uuid::parse_str(source_table_id)
1172            .map_err(|e| invalid_input_error("source_table_id", e))?;
1173        let target_id = uuid::Uuid::parse_str(target_table_id)
1174            .map_err(|e| invalid_input_error("target_table_id", e))?;
1175
1176        let validator = crate::validation::relationships::RelationshipValidator::new();
1177        match validator.check_circular_dependency(&relationships, source_id, target_id) {
1178            Ok((has_cycle, cycle_path)) => {
1179                let cycle_path_strs: Vec<String> = cycle_path
1180                    .map(|path| path.iter().map(|id| id.to_string()).collect())
1181                    .unwrap_or_default();
1182                Ok(serde_json::json!({
1183                    "has_cycle": has_cycle,
1184                    "cycle_path": cycle_path_strs
1185                })
1186                .to_string())
1187            }
1188            Err(err) => Err(validation_error(err)),
1189        }
1190    }
1191
1192    /// Validate that source and target tables are different (no self-reference).
1193    ///
1194    /// # Arguments
1195    ///
1196    /// * `source_table_id` - Source table ID (UUID string)
1197    /// * `target_table_id` - Target table ID (UUID string)
1198    ///
1199    /// # Returns
1200    ///
1201    /// JSON string with validation result: `{"valid": true}` or `{"valid": false, "self_reference": {...}}`
1202    #[wasm_bindgen]
1203    pub fn validate_no_self_reference(
1204        source_table_id: &str,
1205        target_table_id: &str,
1206    ) -> Result<String, JsValue> {
1207        let source_id = uuid::Uuid::parse_str(source_table_id)
1208            .map_err(|e| invalid_input_error("source_table_id", e))?;
1209        let target_id = uuid::Uuid::parse_str(target_table_id)
1210            .map_err(|e| invalid_input_error("target_table_id", e))?;
1211
1212        let validator = crate::validation::relationships::RelationshipValidator::new();
1213        match validator.validate_no_self_reference(source_id, target_id) {
1214            Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1215            Err(self_ref) => {
1216                Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1217            }
1218        }
1219    }
1220
1221    // ============================================================================
1222    // PNG Export
1223    // ============================================================================
1224
1225    /// Export a data model to PNG image format.
1226    ///
1227    /// # Arguments
1228    ///
1229    /// * `workspace_json` - JSON string containing workspace/data model structure
1230    /// * `width` - Image width in pixels
1231    /// * `height` - Image height in pixels
1232    ///
1233    /// # Returns
1234    ///
1235    /// Base64-encoded PNG image string, or JsValue error
1236    #[cfg(feature = "png-export")]
1237    #[wasm_bindgen]
1238    pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1239        let model = deserialize_workspace(workspace_json)?;
1240        let exporter = crate::export::PNGExporter::new();
1241        match exporter.export(&model.tables, width, height) {
1242            Ok(result) => Ok(result.content), // Already base64-encoded
1243            Err(err) => Err(export_error_to_js(err)),
1244        }
1245    }
1246
1247    // ============================================================================
1248    // Model Loading/Saving (Async)
1249    // ============================================================================
1250
1251    /// Load a model from browser storage (IndexedDB/localStorage).
1252    ///
1253    /// # Arguments
1254    ///
1255    /// * `db_name` - IndexedDB database name
1256    /// * `store_name` - Object store name
1257    /// * `workspace_path` - Workspace path to load from
1258    ///
1259    /// # Returns
1260    ///
1261    /// Promise that resolves to JSON string containing ModelLoadResult, or rejects with error
1262    #[wasm_bindgen]
1263    pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1264        let db_name = db_name.to_string();
1265        let store_name = store_name.to_string();
1266        let workspace_path = workspace_path.to_string();
1267
1268        wasm_bindgen_futures::future_to_promise(async move {
1269            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1270            let loader = crate::model::ModelLoader::new(storage);
1271            match loader.load_model(&workspace_path).await {
1272                Ok(result) => serde_json::to_string(&result)
1273                    .map(|s| JsValue::from_str(&s))
1274                    .map_err(serialization_error),
1275                Err(err) => Err(storage_error(err)),
1276            }
1277        })
1278    }
1279
1280    /// Save a model to browser storage (IndexedDB/localStorage).
1281    ///
1282    /// # Arguments
1283    ///
1284    /// * `db_name` - IndexedDB database name
1285    /// * `store_name` - Object store name
1286    /// * `workspace_path` - Workspace path to save to
1287    /// * `model_json` - JSON string containing DataModel to save
1288    ///
1289    /// # Returns
1290    ///
1291    /// Promise that resolves to success message, or rejects with error
1292    #[wasm_bindgen]
1293    pub fn save_model(
1294        db_name: &str,
1295        store_name: &str,
1296        workspace_path: &str,
1297        model_json: &str,
1298    ) -> js_sys::Promise {
1299        let db_name = db_name.to_string();
1300        let store_name = store_name.to_string();
1301        let workspace_path = workspace_path.to_string();
1302        let model_json = model_json.to_string();
1303
1304        wasm_bindgen_futures::future_to_promise(async move {
1305            let model: crate::models::DataModel =
1306                serde_json::from_str(&model_json).map_err(deserialization_error)?;
1307
1308            let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1309            let saver = crate::model::ModelSaver::new(storage);
1310
1311            // Convert DataModel to table/relationship data for saving
1312            // For each table, save as YAML
1313            for table in &model.tables {
1314                // Export table to ODCS YAML
1315                let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1316                let table_data = crate::model::saver::TableData {
1317                    id: table.id,
1318                    name: table.name.clone(),
1319                    yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1320                    yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1321                };
1322                saver
1323                    .save_table(&workspace_path, &table_data)
1324                    .await
1325                    .map_err(storage_error)?;
1326            }
1327
1328            // Save relationships
1329            if !model.relationships.is_empty() {
1330                let rel_data: Vec<crate::model::saver::RelationshipData> = model
1331                    .relationships
1332                    .iter()
1333                    .map(|rel| {
1334                        let yaml_value = serde_json::json!({
1335                            "id": rel.id.to_string(),
1336                            "source_table_id": rel.source_table_id.to_string(),
1337                            "target_table_id": rel.target_table_id.to_string(),
1338                        });
1339                        // Convert JSON value to YAML value
1340                        let yaml_str = serde_json::to_string(&yaml_value)
1341                            .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1342                        let yaml_value = serde_yaml::from_str(&yaml_str)
1343                            .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1344                        Ok(crate::model::saver::RelationshipData {
1345                            id: rel.id,
1346                            source_table_id: rel.source_table_id,
1347                            target_table_id: rel.target_table_id,
1348                            yaml_value,
1349                        })
1350                    })
1351                    .collect::<Result<Vec<_>, String>>()
1352                    .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1353
1354                saver
1355                    .save_relationships(&workspace_path, &rel_data)
1356                    .await
1357                    .map_err(|e| storage_error(e))?;
1358            }
1359
1360            Ok(JsValue::from_str("Model saved successfully"))
1361        })
1362    }
1363
1364    // BPMN WASM Bindings
1365    /// Import a BPMN model from XML content.
1366    ///
1367    /// # Arguments
1368    ///
1369    /// * `domain_id` - Domain UUID as string
1370    /// * `xml_content` - BPMN XML content as a string
1371    /// * `model_name` - Optional model name (extracted from XML if not provided)
1372    ///
1373    /// # Returns
1374    ///
1375    /// JSON string containing BPMNModel, or JsValue error
1376    #[cfg(feature = "bpmn")]
1377    #[wasm_bindgen]
1378    pub fn import_bpmn_model(
1379        domain_id: &str,
1380        xml_content: &str,
1381        model_name: Option<String>,
1382    ) -> Result<String, JsValue> {
1383        use crate::import::bpmn::BPMNImporter;
1384        use uuid::Uuid;
1385
1386        let domain_uuid =
1387            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1388
1389        let mut importer = BPMNImporter::new();
1390        match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1391            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1392            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1393        }
1394    }
1395
1396    /// Export a BPMN model to XML content.
1397    ///
1398    /// # Arguments
1399    ///
1400    /// * `xml_content` - BPMN XML content as a string
1401    ///
1402    /// # Returns
1403    ///
1404    /// BPMN XML content as string, or JsValue error
1405    #[cfg(feature = "bpmn")]
1406    #[wasm_bindgen]
1407    pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1408        use crate::export::bpmn::BPMNExporter;
1409        let exporter = BPMNExporter::new();
1410        exporter
1411            .export(xml_content)
1412            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1413    }
1414
1415    // DMN WASM Bindings
1416    /// Import a DMN model from XML content.
1417    ///
1418    /// # Arguments
1419    ///
1420    /// * `domain_id` - Domain UUID as string
1421    /// * `xml_content` - DMN XML content as a string
1422    /// * `model_name` - Optional model name (extracted from XML if not provided)
1423    ///
1424    /// # Returns
1425    ///
1426    /// JSON string containing DMNModel, or JsValue error
1427    #[cfg(feature = "dmn")]
1428    #[wasm_bindgen]
1429    pub fn import_dmn_model(
1430        domain_id: &str,
1431        xml_content: &str,
1432        model_name: Option<String>,
1433    ) -> Result<String, JsValue> {
1434        use crate::import::dmn::DMNImporter;
1435        use uuid::Uuid;
1436
1437        let domain_uuid =
1438            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1439
1440        let mut importer = DMNImporter::new();
1441        match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1442            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1443            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1444        }
1445    }
1446
1447    /// Export a DMN model to XML content.
1448    ///
1449    /// # Arguments
1450    ///
1451    /// * `xml_content` - DMN XML content as a string
1452    ///
1453    /// # Returns
1454    ///
1455    /// DMN XML content as string, or JsValue error
1456    #[cfg(feature = "dmn")]
1457    #[wasm_bindgen]
1458    pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1459        use crate::export::dmn::DMNExporter;
1460        let exporter = DMNExporter::new();
1461        exporter
1462            .export(xml_content)
1463            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1464    }
1465
1466    // OpenAPI WASM Bindings
1467    /// Import an OpenAPI specification from YAML or JSON content.
1468    ///
1469    /// # Arguments
1470    ///
1471    /// * `domain_id` - Domain UUID as string
1472    /// * `content` - OpenAPI YAML or JSON content as a string
1473    /// * `api_name` - Optional API name (extracted from info.title if not provided)
1474    ///
1475    /// # Returns
1476    ///
1477    /// JSON string containing OpenAPIModel, or JsValue error
1478    #[cfg(feature = "openapi")]
1479    #[wasm_bindgen]
1480    pub fn import_openapi_spec(
1481        domain_id: &str,
1482        content: &str,
1483        api_name: Option<String>,
1484    ) -> Result<String, JsValue> {
1485        use crate::import::openapi::OpenAPIImporter;
1486        use uuid::Uuid;
1487
1488        let domain_uuid =
1489            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1490
1491        let mut importer = OpenAPIImporter::new();
1492        match importer.import(content, domain_uuid, api_name.as_deref()) {
1493            Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1494            Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1495        }
1496    }
1497
1498    /// Export an OpenAPI specification to YAML or JSON content.
1499    ///
1500    /// # Arguments
1501    ///
1502    /// * `content` - OpenAPI content as a string
1503    /// * `source_format` - Source format ("yaml" or "json")
1504    /// * `target_format` - Optional target format for conversion (None to keep original)
1505    ///
1506    /// # Returns
1507    ///
1508    /// OpenAPI content in requested format, or JsValue error
1509    #[cfg(feature = "openapi")]
1510    #[wasm_bindgen]
1511    pub fn export_openapi_spec(
1512        content: &str,
1513        source_format: &str,
1514        target_format: Option<String>,
1515    ) -> Result<String, JsValue> {
1516        use crate::export::openapi::OpenAPIExporter;
1517        use crate::models::openapi::OpenAPIFormat;
1518
1519        let source_fmt = match source_format {
1520            "yaml" | "yml" => OpenAPIFormat::Yaml,
1521            "json" => OpenAPIFormat::Json,
1522            _ => {
1523                return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1524            }
1525        };
1526
1527        let target_fmt = if let Some(tf) = target_format {
1528            match tf.as_str() {
1529                "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1530                "json" => Some(OpenAPIFormat::Json),
1531                _ => {
1532                    return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1533                }
1534            }
1535        } else {
1536            None
1537        };
1538
1539        let exporter = OpenAPIExporter::new();
1540        exporter
1541            .export(content, source_fmt, target_fmt)
1542            .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1543    }
1544
1545    /// Convert an OpenAPI schema component to an ODCS table.
1546    ///
1547    /// # Arguments
1548    ///
1549    /// * `openapi_content` - OpenAPI YAML or JSON content as a string
1550    /// * `component_name` - Name of the schema component to convert
1551    /// * `table_name` - Optional desired ODCS table name (uses component_name if None)
1552    ///
1553    /// # Returns
1554    ///
1555    /// JSON string containing ODCS Table, or JsValue error
1556    #[cfg(feature = "openapi")]
1557    #[wasm_bindgen]
1558    pub fn convert_openapi_to_odcs(
1559        openapi_content: &str,
1560        component_name: &str,
1561        table_name: Option<String>,
1562    ) -> Result<String, JsValue> {
1563        use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1564
1565        let converter = OpenAPIToODCSConverter::new();
1566        match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1567            Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1568            Err(e) => Err(conversion_error(e)),
1569        }
1570    }
1571
1572    /// Analyze an OpenAPI component for conversion feasibility.
1573    ///
1574    /// # Arguments
1575    ///
1576    /// * `openapi_content` - OpenAPI YAML or JSON content as a string
1577    /// * `component_name` - Name of the schema component to analyze
1578    ///
1579    /// # Returns
1580    ///
1581    /// JSON string containing ConversionReport, or JsValue error
1582    #[cfg(feature = "openapi")]
1583    #[wasm_bindgen]
1584    pub fn analyze_openapi_conversion(
1585        openapi_content: &str,
1586        component_name: &str,
1587    ) -> Result<String, JsValue> {
1588        use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1589
1590        let converter = OpenAPIToODCSConverter::new();
1591        match converter.analyze_conversion(openapi_content, component_name) {
1592            Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1593            Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1594                .with_code("ANALYSIS_FAILED")
1595                .to_js_value()),
1596        }
1597    }
1598
1599    // ============================================================================
1600    // Workspace and DomainConfig Operations
1601    // ============================================================================
1602
1603    /// Create a new workspace.
1604    ///
1605    /// # Arguments
1606    ///
1607    /// * `name` - Workspace name
1608    /// * `owner_id` - Owner UUID as string
1609    ///
1610    /// # Returns
1611    ///
1612    /// JSON string containing Workspace, or JsValue error
1613    #[wasm_bindgen]
1614    pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1615        use crate::models::workspace::Workspace;
1616        use chrono::Utc;
1617        use uuid::Uuid;
1618
1619        let owner_uuid =
1620            Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1621
1622        let workspace = Workspace::new(name.to_string(), owner_uuid);
1623
1624        serde_json::to_string(&workspace).map_err(serialization_error)
1625    }
1626
1627    /// Parse workspace YAML content and return a structured representation.
1628    ///
1629    /// # Arguments
1630    ///
1631    /// * `yaml_content` - Workspace YAML content as a string
1632    ///
1633    /// # Returns
1634    ///
1635    /// JSON string containing Workspace, or JsValue error
1636    #[wasm_bindgen]
1637    pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1638        use crate::models::workspace::Workspace;
1639
1640        let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1641        serde_json::to_string(&workspace).map_err(serialization_error)
1642    }
1643
1644    /// Export a workspace to YAML format.
1645    ///
1646    /// # Arguments
1647    ///
1648    /// * `workspace_json` - JSON string containing Workspace
1649    ///
1650    /// # Returns
1651    ///
1652    /// Workspace YAML format string, or JsValue error
1653    #[wasm_bindgen]
1654    pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1655        use crate::models::workspace::Workspace;
1656
1657        let workspace: Workspace =
1658            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1659        serde_yaml::to_string(&workspace).map_err(serialization_error)
1660    }
1661
1662    /// Add a domain reference to a workspace.
1663    ///
1664    /// # Arguments
1665    ///
1666    /// * `workspace_json` - JSON string containing Workspace
1667    /// * `domain_id` - Domain UUID as string
1668    /// * `domain_name` - Domain name
1669    ///
1670    /// # Returns
1671    ///
1672    /// JSON string containing updated Workspace, or JsValue error
1673    #[wasm_bindgen]
1674    pub fn add_domain_to_workspace(
1675        workspace_json: &str,
1676        domain_id: &str,
1677        domain_name: &str,
1678    ) -> Result<String, JsValue> {
1679        use crate::models::workspace::{DomainReference, Workspace};
1680        use chrono::Utc;
1681        use uuid::Uuid;
1682
1683        let mut workspace: Workspace =
1684            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1685        let domain_uuid =
1686            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1687
1688        // Check if domain already exists
1689        if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1690            return Err(WasmError::new(
1691                "DuplicateError",
1692                format!("Domain {} already exists in workspace", domain_id),
1693            )
1694            .with_code("DUPLICATE_DOMAIN")
1695            .to_js_value());
1696        }
1697
1698        workspace.domains.push(DomainReference {
1699            id: domain_uuid,
1700            name: domain_name.to_string(),
1701            description: None,
1702            systems: Vec::new(),
1703        });
1704        workspace.last_modified_at = Utc::now();
1705
1706        serde_json::to_string(&workspace).map_err(serialization_error)
1707    }
1708
1709    /// Remove a domain reference from a workspace.
1710    ///
1711    /// # Arguments
1712    ///
1713    /// * `workspace_json` - JSON string containing Workspace
1714    /// * `domain_id` - Domain UUID as string to remove
1715    ///
1716    /// # Returns
1717    ///
1718    /// JSON string containing updated Workspace, or JsValue error
1719    #[wasm_bindgen]
1720    pub fn remove_domain_from_workspace(
1721        workspace_json: &str,
1722        domain_id: &str,
1723    ) -> Result<String, JsValue> {
1724        use crate::models::workspace::Workspace;
1725        use chrono::Utc;
1726        use uuid::Uuid;
1727
1728        let mut workspace: Workspace =
1729            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1730        let domain_uuid =
1731            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1732
1733        let original_len = workspace.domains.len();
1734        workspace.domains.retain(|d| d.id != domain_uuid);
1735
1736        if workspace.domains.len() == original_len {
1737            return Err(WasmError::new(
1738                "NotFoundError",
1739                format!("Domain {} not found in workspace", domain_id),
1740            )
1741            .with_code("DOMAIN_NOT_FOUND")
1742            .to_js_value());
1743        }
1744
1745        workspace.last_modified_at = Utc::now();
1746        serde_json::to_string(&workspace).map_err(serialization_error)
1747    }
1748
1749    /// Add a relationship to a workspace.
1750    ///
1751    /// # Arguments
1752    ///
1753    /// * `workspace_json` - JSON string containing Workspace
1754    /// * `relationship_json` - JSON string containing Relationship
1755    ///
1756    /// # Returns
1757    ///
1758    /// JSON string containing updated Workspace, or JsValue error
1759    #[wasm_bindgen]
1760    pub fn add_relationship_to_workspace(
1761        workspace_json: &str,
1762        relationship_json: &str,
1763    ) -> Result<String, JsValue> {
1764        use crate::models::Relationship;
1765        use crate::models::workspace::Workspace;
1766
1767        let mut workspace: Workspace =
1768            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1769        let relationship: Relationship =
1770            serde_json::from_str(relationship_json).map_err(deserialization_error)?;
1771
1772        // Check if relationship already exists
1773        if workspace
1774            .relationships
1775            .iter()
1776            .any(|r| r.id == relationship.id)
1777        {
1778            return Err(WasmError::new(
1779                "DuplicateError",
1780                format!(
1781                    "Relationship {} already exists in workspace",
1782                    relationship.id
1783                ),
1784            )
1785            .with_code("DUPLICATE_RELATIONSHIP")
1786            .to_js_value());
1787        }
1788
1789        workspace.add_relationship(relationship);
1790        serde_json::to_string(&workspace).map_err(serialization_error)
1791    }
1792
1793    /// Remove a relationship from a workspace.
1794    ///
1795    /// # Arguments
1796    ///
1797    /// * `workspace_json` - JSON string containing Workspace
1798    /// * `relationship_id` - Relationship UUID as string to remove
1799    ///
1800    /// # Returns
1801    ///
1802    /// JSON string containing updated Workspace, or JsValue error
1803    #[wasm_bindgen]
1804    pub fn remove_relationship_from_workspace(
1805        workspace_json: &str,
1806        relationship_id: &str,
1807    ) -> Result<String, JsValue> {
1808        use crate::models::workspace::Workspace;
1809        use uuid::Uuid;
1810
1811        let mut workspace: Workspace =
1812            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1813        let relationship_uuid = Uuid::parse_str(relationship_id)
1814            .map_err(|e| invalid_input_error("relationship ID", e))?;
1815
1816        if !workspace.remove_relationship(relationship_uuid) {
1817            return Err(WasmError::new(
1818                "NotFoundError",
1819                format!("Relationship {} not found in workspace", relationship_id),
1820            )
1821            .with_code("RELATIONSHIP_NOT_FOUND")
1822            .to_js_value());
1823        }
1824
1825        serde_json::to_string(&workspace).map_err(serialization_error)
1826    }
1827
1828    /// Get relationships for a source table from a workspace.
1829    ///
1830    /// # Arguments
1831    ///
1832    /// * `workspace_json` - JSON string containing Workspace
1833    /// * `source_table_id` - Source table UUID as string
1834    ///
1835    /// # Returns
1836    ///
1837    /// JSON string containing array of Relationships, or JsValue error
1838    #[wasm_bindgen]
1839    pub fn get_workspace_relationships_for_source(
1840        workspace_json: &str,
1841        source_table_id: &str,
1842    ) -> Result<String, JsValue> {
1843        use crate::models::workspace::Workspace;
1844        use uuid::Uuid;
1845
1846        let workspace: Workspace =
1847            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1848        let source_uuid = Uuid::parse_str(source_table_id)
1849            .map_err(|e| invalid_input_error("source table ID", e))?;
1850
1851        let relationships: Vec<_> = workspace.get_relationships_for_source(source_uuid);
1852        serde_json::to_string(&relationships).map_err(serialization_error)
1853    }
1854
1855    /// Get relationships for a target table from a workspace.
1856    ///
1857    /// # Arguments
1858    ///
1859    /// * `workspace_json` - JSON string containing Workspace
1860    /// * `target_table_id` - Target 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_target(
1867        workspace_json: &str,
1868        target_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 target_uuid = Uuid::parse_str(target_table_id)
1876            .map_err(|e| invalid_input_error("target table ID", e))?;
1877
1878        let relationships: Vec<_> = workspace.get_relationships_for_target(target_uuid);
1879        serde_json::to_string(&relationships).map_err(serialization_error)
1880    }
1881
1882    /// Create a new domain configuration.
1883    ///
1884    /// # Arguments
1885    ///
1886    /// * `name` - Domain name
1887    /// * `workspace_id` - Workspace UUID as string
1888    ///
1889    /// # Returns
1890    ///
1891    /// JSON string containing DomainConfig, or JsValue error
1892    #[wasm_bindgen]
1893    pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1894        use crate::models::domain_config::DomainConfig;
1895        use chrono::Utc;
1896        use std::collections::HashMap;
1897        use uuid::Uuid;
1898
1899        let workspace_uuid =
1900            Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1901
1902        let config = DomainConfig {
1903            id: Uuid::new_v4(),
1904            workspace_id: workspace_uuid,
1905            name: name.to_string(),
1906            description: None,
1907            created_at: Utc::now(),
1908            last_modified_at: Utc::now(),
1909            owner: None,
1910            systems: Vec::new(),
1911            tables: Vec::new(),
1912            products: Vec::new(),
1913            assets: Vec::new(),
1914            processes: Vec::new(),
1915            decisions: Vec::new(),
1916            view_positions: HashMap::new(),
1917            folder_path: None,
1918            workspace_path: None,
1919        };
1920
1921        serde_json::to_string(&config).map_err(serialization_error)
1922    }
1923
1924    /// Parse domain config YAML content and return a structured representation.
1925    ///
1926    /// # Arguments
1927    ///
1928    /// * `yaml_content` - Domain config YAML content as a string
1929    ///
1930    /// # Returns
1931    ///
1932    /// JSON string containing DomainConfig, or JsValue error
1933    #[wasm_bindgen]
1934    pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1935        use crate::models::domain_config::DomainConfig;
1936
1937        let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1938        serde_json::to_string(&config).map_err(serialization_error)
1939    }
1940
1941    /// Export a domain config to YAML format.
1942    ///
1943    /// # Arguments
1944    ///
1945    /// * `config_json` - JSON string containing DomainConfig
1946    ///
1947    /// # Returns
1948    ///
1949    /// DomainConfig YAML format string, or JsValue error
1950    #[wasm_bindgen]
1951    pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1952        use crate::models::domain_config::DomainConfig;
1953
1954        let config: DomainConfig =
1955            serde_json::from_str(config_json).map_err(deserialization_error)?;
1956        serde_yaml::to_string(&config).map_err(serialization_error)
1957    }
1958
1959    /// Get the domain ID from a domain config JSON.
1960    ///
1961    /// # Arguments
1962    ///
1963    /// * `config_json` - JSON string containing DomainConfig
1964    ///
1965    /// # Returns
1966    ///
1967    /// Domain UUID as string, or JsValue error
1968    #[wasm_bindgen]
1969    pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1970        use crate::models::domain_config::DomainConfig;
1971
1972        let config: DomainConfig =
1973            serde_json::from_str(config_json).map_err(deserialization_error)?;
1974        Ok(config.id.to_string())
1975    }
1976
1977    /// Update domain config with new view positions.
1978    ///
1979    /// # Arguments
1980    ///
1981    /// * `config_json` - JSON string containing DomainConfig
1982    /// * `positions_json` - JSON string containing view positions map
1983    ///
1984    /// # Returns
1985    ///
1986    /// JSON string containing updated DomainConfig, or JsValue error
1987    #[wasm_bindgen]
1988    pub fn update_domain_view_positions(
1989        config_json: &str,
1990        positions_json: &str,
1991    ) -> Result<String, JsValue> {
1992        use crate::models::domain_config::{DomainConfig, ViewPosition};
1993        use chrono::Utc;
1994        use std::collections::HashMap;
1995
1996        let mut config: DomainConfig =
1997            serde_json::from_str(config_json).map_err(deserialization_error)?;
1998        let positions: HashMap<String, HashMap<String, ViewPosition>> =
1999            serde_json::from_str(positions_json).map_err(deserialization_error)?;
2000
2001        config.view_positions = positions;
2002        config.last_modified_at = Utc::now();
2003
2004        serde_json::to_string(&config).map_err(serialization_error)
2005    }
2006
2007    /// Add an entity reference to a domain config.
2008    ///
2009    /// # Arguments
2010    ///
2011    /// * `config_json` - JSON string containing DomainConfig
2012    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2013    /// * `entity_id` - Entity UUID as string
2014    ///
2015    /// # Returns
2016    ///
2017    /// JSON string containing updated DomainConfig, or JsValue error
2018    #[wasm_bindgen]
2019    pub fn add_entity_to_domain_config(
2020        config_json: &str,
2021        entity_type: &str,
2022        entity_id: &str,
2023    ) -> Result<String, JsValue> {
2024        use crate::models::domain_config::DomainConfig;
2025        use chrono::Utc;
2026        use uuid::Uuid;
2027
2028        let mut config: DomainConfig =
2029            serde_json::from_str(config_json).map_err(deserialization_error)?;
2030        let entity_uuid =
2031            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2032
2033        let entities = match entity_type {
2034            "system" => &mut config.systems,
2035            "table" => &mut config.tables,
2036            "product" => &mut config.products,
2037            "asset" => &mut config.assets,
2038            "process" => &mut config.processes,
2039            "decision" => &mut config.decisions,
2040            _ => {
2041                return Err(invalid_input_error(
2042                    "entity type",
2043                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2044                ));
2045            }
2046        };
2047
2048        if entities.contains(&entity_uuid) {
2049            return Err(WasmError::new(
2050                "DuplicateError",
2051                format!(
2052                    "{} {} already exists in domain config",
2053                    entity_type, entity_id
2054                ),
2055            )
2056            .with_code("DUPLICATE_ENTITY")
2057            .to_js_value());
2058        }
2059
2060        entities.push(entity_uuid);
2061        config.last_modified_at = Utc::now();
2062
2063        serde_json::to_string(&config).map_err(serialization_error)
2064    }
2065
2066    /// Remove an entity reference from a domain config.
2067    ///
2068    /// # Arguments
2069    ///
2070    /// * `config_json` - JSON string containing DomainConfig
2071    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2072    /// * `entity_id` - Entity UUID as string to remove
2073    ///
2074    /// # Returns
2075    ///
2076    /// JSON string containing updated DomainConfig, or JsValue error
2077    #[wasm_bindgen]
2078    pub fn remove_entity_from_domain_config(
2079        config_json: &str,
2080        entity_type: &str,
2081        entity_id: &str,
2082    ) -> Result<String, JsValue> {
2083        use crate::models::domain_config::DomainConfig;
2084        use chrono::Utc;
2085        use uuid::Uuid;
2086
2087        let mut config: DomainConfig =
2088            serde_json::from_str(config_json).map_err(deserialization_error)?;
2089        let entity_uuid =
2090            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2091
2092        let entities = match entity_type {
2093            "system" => &mut config.systems,
2094            "table" => &mut config.tables,
2095            "product" => &mut config.products,
2096            "asset" => &mut config.assets,
2097            "process" => &mut config.processes,
2098            "decision" => &mut config.decisions,
2099            _ => {
2100                return Err(invalid_input_error(
2101                    "entity type",
2102                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2103                ));
2104            }
2105        };
2106
2107        let original_len = entities.len();
2108        entities.retain(|id| *id != entity_uuid);
2109
2110        if entities.len() == original_len {
2111            return Err(WasmError::new(
2112                "NotFoundError",
2113                format!("{} {} not found in domain config", entity_type, entity_id),
2114            )
2115            .with_code("ENTITY_NOT_FOUND")
2116            .to_js_value());
2117        }
2118
2119        config.last_modified_at = Utc::now();
2120        serde_json::to_string(&config).map_err(serialization_error)
2121    }
2122
2123    // ============================================================================
2124    // Decision Log (DDL) Operations
2125    // ============================================================================
2126
2127    /// Parse a decision YAML file and return a structured representation.
2128    ///
2129    /// # Arguments
2130    ///
2131    /// * `yaml_content` - Decision YAML content as a string (.madr.yaml)
2132    ///
2133    /// # Returns
2134    ///
2135    /// JSON string containing Decision, or JsValue error
2136    #[wasm_bindgen]
2137    pub fn parse_decision_yaml(yaml_content: &str) -> Result<String, JsValue> {
2138        use crate::import::decision::DecisionImporter;
2139
2140        let importer = DecisionImporter::new();
2141        match importer.import(yaml_content) {
2142            Ok(decision) => serde_json::to_string(&decision).map_err(serialization_error),
2143            Err(e) => Err(import_error_to_js(e)),
2144        }
2145    }
2146
2147    /// Parse a decisions index YAML file and return a structured representation.
2148    ///
2149    /// # Arguments
2150    ///
2151    /// * `yaml_content` - Decisions index YAML content as a string (decisions.yaml)
2152    ///
2153    /// # Returns
2154    ///
2155    /// JSON string containing DecisionIndex, or JsValue error
2156    #[wasm_bindgen]
2157    pub fn parse_decision_index_yaml(yaml_content: &str) -> Result<String, JsValue> {
2158        use crate::import::decision::DecisionImporter;
2159
2160        let importer = DecisionImporter::new();
2161        match importer.import_index(yaml_content) {
2162            Ok(index) => serde_json::to_string(&index).map_err(serialization_error),
2163            Err(e) => Err(import_error_to_js(e)),
2164        }
2165    }
2166
2167    /// Export a decision to YAML format.
2168    ///
2169    /// # Arguments
2170    ///
2171    /// * `decision_json` - JSON string containing Decision
2172    ///
2173    /// # Returns
2174    ///
2175    /// Decision YAML format string, or JsValue error
2176    #[wasm_bindgen]
2177    pub fn export_decision_to_yaml(decision_json: &str) -> Result<String, JsValue> {
2178        use crate::export::decision::DecisionExporter;
2179        use crate::models::decision::Decision;
2180
2181        let decision: Decision =
2182            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2183        let exporter = DecisionExporter::new();
2184        exporter
2185            .export_without_validation(&decision)
2186            .map_err(export_error_to_js)
2187    }
2188
2189    /// Export a decisions index to YAML format.
2190    ///
2191    /// # Arguments
2192    ///
2193    /// * `index_json` - JSON string containing DecisionIndex
2194    ///
2195    /// # Returns
2196    ///
2197    /// DecisionIndex YAML format string, or JsValue error
2198    #[wasm_bindgen]
2199    pub fn export_decision_index_to_yaml(index_json: &str) -> Result<String, JsValue> {
2200        use crate::export::decision::DecisionExporter;
2201        use crate::models::decision::DecisionIndex;
2202
2203        let index: DecisionIndex =
2204            serde_json::from_str(index_json).map_err(deserialization_error)?;
2205        let exporter = DecisionExporter::new();
2206        exporter.export_index(&index).map_err(export_error_to_js)
2207    }
2208
2209    /// Export a decision to Markdown format (MADR template).
2210    ///
2211    /// # Arguments
2212    ///
2213    /// * `decision_json` - JSON string containing Decision
2214    ///
2215    /// # Returns
2216    ///
2217    /// Decision Markdown string, or JsValue error
2218    #[wasm_bindgen]
2219    pub fn export_decision_to_markdown(decision_json: &str) -> Result<String, JsValue> {
2220        use crate::export::markdown::MarkdownExporter;
2221        use crate::models::decision::Decision;
2222
2223        let decision: Decision =
2224            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2225        let exporter = MarkdownExporter::new();
2226        exporter
2227            .export_decision(&decision)
2228            .map_err(export_error_to_js)
2229    }
2230
2231    /// Create a new decision with required fields.
2232    ///
2233    /// # Arguments
2234    ///
2235    /// * `number` - Decision number (ADR-0001, ADR-0002, etc.)
2236    /// * `title` - Short title describing the decision
2237    /// * `context` - Problem statement and context
2238    /// * `decision` - The decision that was made
2239    ///
2240    /// # Returns
2241    ///
2242    /// JSON string containing Decision, or JsValue error
2243    #[wasm_bindgen]
2244    pub fn create_decision(
2245        number: u32,
2246        title: &str,
2247        context: &str,
2248        decision: &str,
2249    ) -> Result<String, JsValue> {
2250        use crate::models::decision::Decision;
2251
2252        let dec = Decision::new(number, title, context, decision);
2253        serde_json::to_string(&dec).map_err(serialization_error)
2254    }
2255
2256    /// Create a new empty decision index.
2257    ///
2258    /// # Returns
2259    ///
2260    /// JSON string containing DecisionIndex, or JsValue error
2261    #[wasm_bindgen]
2262    pub fn create_decision_index() -> Result<String, JsValue> {
2263        use crate::models::decision::DecisionIndex;
2264
2265        let index = DecisionIndex::new();
2266        serde_json::to_string(&index).map_err(serialization_error)
2267    }
2268
2269    /// Add a decision to an index.
2270    ///
2271    /// # Arguments
2272    ///
2273    /// * `index_json` - JSON string containing DecisionIndex
2274    /// * `decision_json` - JSON string containing Decision
2275    /// * `filename` - Filename for the decision YAML file
2276    ///
2277    /// # Returns
2278    ///
2279    /// JSON string containing updated DecisionIndex, or JsValue error
2280    #[wasm_bindgen]
2281    pub fn add_decision_to_index(
2282        index_json: &str,
2283        decision_json: &str,
2284        filename: &str,
2285    ) -> Result<String, JsValue> {
2286        use crate::models::decision::{Decision, DecisionIndex};
2287
2288        let mut index: DecisionIndex =
2289            serde_json::from_str(index_json).map_err(deserialization_error)?;
2290        let decision: Decision =
2291            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2292
2293        index.add_decision(&decision, filename.to_string());
2294        serde_json::to_string(&index).map_err(serialization_error)
2295    }
2296
2297    // ============================================================================
2298    // Knowledge Base (KB) Operations
2299    // ============================================================================
2300
2301    /// Parse a knowledge article YAML file and return a structured representation.
2302    ///
2303    /// # Arguments
2304    ///
2305    /// * `yaml_content` - Knowledge article YAML content as a string (.kb.yaml)
2306    ///
2307    /// # Returns
2308    ///
2309    /// JSON string containing KnowledgeArticle, or JsValue error
2310    #[wasm_bindgen]
2311    pub fn parse_knowledge_yaml(yaml_content: &str) -> Result<String, JsValue> {
2312        use crate::import::knowledge::KnowledgeImporter;
2313
2314        let importer = KnowledgeImporter::new();
2315        match importer.import(yaml_content) {
2316            Ok(article) => serde_json::to_string(&article).map_err(serialization_error),
2317            Err(e) => Err(import_error_to_js(e)),
2318        }
2319    }
2320
2321    /// Parse a knowledge index YAML file and return a structured representation.
2322    ///
2323    /// # Arguments
2324    ///
2325    /// * `yaml_content` - Knowledge index YAML content as a string (knowledge.yaml)
2326    ///
2327    /// # Returns
2328    ///
2329    /// JSON string containing KnowledgeIndex, or JsValue error
2330    #[wasm_bindgen]
2331    pub fn parse_knowledge_index_yaml(yaml_content: &str) -> Result<String, JsValue> {
2332        use crate::import::knowledge::KnowledgeImporter;
2333
2334        let importer = KnowledgeImporter::new();
2335        match importer.import_index(yaml_content) {
2336            Ok(index) => serde_json::to_string(&index).map_err(serialization_error),
2337            Err(e) => Err(import_error_to_js(e)),
2338        }
2339    }
2340
2341    /// Export a knowledge article to YAML format.
2342    ///
2343    /// # Arguments
2344    ///
2345    /// * `article_json` - JSON string containing KnowledgeArticle
2346    ///
2347    /// # Returns
2348    ///
2349    /// KnowledgeArticle YAML format string, or JsValue error
2350    #[wasm_bindgen]
2351    pub fn export_knowledge_to_yaml(article_json: &str) -> Result<String, JsValue> {
2352        use crate::export::knowledge::KnowledgeExporter;
2353        use crate::models::knowledge::KnowledgeArticle;
2354
2355        let article: KnowledgeArticle =
2356            serde_json::from_str(article_json).map_err(deserialization_error)?;
2357        let exporter = KnowledgeExporter::new();
2358        exporter
2359            .export_without_validation(&article)
2360            .map_err(export_error_to_js)
2361    }
2362
2363    /// Export a knowledge index to YAML format.
2364    ///
2365    /// # Arguments
2366    ///
2367    /// * `index_json` - JSON string containing KnowledgeIndex
2368    ///
2369    /// # Returns
2370    ///
2371    /// KnowledgeIndex YAML format string, or JsValue error
2372    #[wasm_bindgen]
2373    pub fn export_knowledge_index_to_yaml(index_json: &str) -> Result<String, JsValue> {
2374        use crate::export::knowledge::KnowledgeExporter;
2375        use crate::models::knowledge::KnowledgeIndex;
2376
2377        let index: KnowledgeIndex =
2378            serde_json::from_str(index_json).map_err(deserialization_error)?;
2379        let exporter = KnowledgeExporter::new();
2380        exporter.export_index(&index).map_err(export_error_to_js)
2381    }
2382
2383    /// Export a knowledge article to Markdown format.
2384    ///
2385    /// # Arguments
2386    ///
2387    /// * `article_json` - JSON string containing KnowledgeArticle
2388    ///
2389    /// # Returns
2390    ///
2391    /// KnowledgeArticle Markdown string, or JsValue error
2392    #[wasm_bindgen]
2393    pub fn export_knowledge_to_markdown(article_json: &str) -> Result<String, JsValue> {
2394        use crate::export::markdown::MarkdownExporter;
2395        use crate::models::knowledge::KnowledgeArticle;
2396
2397        let article: KnowledgeArticle =
2398            serde_json::from_str(article_json).map_err(deserialization_error)?;
2399        let exporter = MarkdownExporter::new();
2400        exporter
2401            .export_knowledge(&article)
2402            .map_err(export_error_to_js)
2403    }
2404
2405    /// Create a new knowledge article with required fields.
2406    ///
2407    /// # Arguments
2408    ///
2409    /// * `number` - Article number (1, 2, 3, etc. - will be formatted as KB-0001)
2410    /// * `title` - Article title
2411    /// * `summary` - Brief summary of the article
2412    /// * `content` - Full article content in Markdown
2413    /// * `author` - Article author (email or name)
2414    ///
2415    /// # Returns
2416    ///
2417    /// JSON string containing KnowledgeArticle, or JsValue error
2418    #[wasm_bindgen]
2419    pub fn create_knowledge_article(
2420        number: u32,
2421        title: &str,
2422        summary: &str,
2423        content: &str,
2424        author: &str,
2425    ) -> Result<String, JsValue> {
2426        use crate::models::knowledge::KnowledgeArticle;
2427
2428        let article = KnowledgeArticle::new(number, title, summary, content, author);
2429        serde_json::to_string(&article).map_err(serialization_error)
2430    }
2431
2432    /// Create a new empty knowledge index.
2433    ///
2434    /// # Returns
2435    ///
2436    /// JSON string containing KnowledgeIndex, or JsValue error
2437    #[wasm_bindgen]
2438    pub fn create_knowledge_index() -> Result<String, JsValue> {
2439        use crate::models::knowledge::KnowledgeIndex;
2440
2441        let index = KnowledgeIndex::new();
2442        serde_json::to_string(&index).map_err(serialization_error)
2443    }
2444
2445    /// Add an article to a knowledge index.
2446    ///
2447    /// # Arguments
2448    ///
2449    /// * `index_json` - JSON string containing KnowledgeIndex
2450    /// * `article_json` - JSON string containing KnowledgeArticle
2451    /// * `filename` - Filename for the article YAML file
2452    ///
2453    /// # Returns
2454    ///
2455    /// JSON string containing updated KnowledgeIndex, or JsValue error
2456    #[wasm_bindgen]
2457    pub fn add_article_to_knowledge_index(
2458        index_json: &str,
2459        article_json: &str,
2460        filename: &str,
2461    ) -> Result<String, JsValue> {
2462        use crate::models::knowledge::{KnowledgeArticle, KnowledgeIndex};
2463
2464        let mut index: KnowledgeIndex =
2465            serde_json::from_str(index_json).map_err(deserialization_error)?;
2466        let article: KnowledgeArticle =
2467            serde_json::from_str(article_json).map_err(deserialization_error)?;
2468
2469        index.add_article(&article, filename.to_string());
2470        serde_json::to_string(&index).map_err(serialization_error)
2471    }
2472
2473    /// Search knowledge articles by title, summary, or content.
2474    ///
2475    /// # Arguments
2476    ///
2477    /// * `articles_json` - JSON string containing array of KnowledgeArticle
2478    /// * `query` - Search query string (case-insensitive)
2479    ///
2480    /// # Returns
2481    ///
2482    /// JSON string containing array of matching KnowledgeArticle, or JsValue error
2483    #[wasm_bindgen]
2484    pub fn search_knowledge_articles(articles_json: &str, query: &str) -> Result<String, JsValue> {
2485        use crate::models::knowledge::KnowledgeArticle;
2486
2487        let articles: Vec<KnowledgeArticle> =
2488            serde_json::from_str(articles_json).map_err(deserialization_error)?;
2489
2490        let query_lower = query.to_lowercase();
2491        let matches: Vec<&KnowledgeArticle> = articles
2492            .iter()
2493            .filter(|article| {
2494                article.title.to_lowercase().contains(&query_lower)
2495                    || article.summary.to_lowercase().contains(&query_lower)
2496                    || article.content.to_lowercase().contains(&query_lower)
2497                    || article
2498                        .tags
2499                        .iter()
2500                        .any(|tag| tag.to_string().to_lowercase().contains(&query_lower))
2501            })
2502            .collect();
2503
2504        serde_json::to_string(&matches).map_err(serialization_error)
2505    }
2506}