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