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            view_positions: std::collections::HashMap::new(),
1724        });
1725        workspace.last_modified_at = Utc::now();
1726
1727        serde_json::to_string(&workspace).map_err(serialization_error)
1728    }
1729
1730    /// Remove a domain reference from a workspace.
1731    ///
1732    /// # Arguments
1733    ///
1734    /// * `workspace_json` - JSON string containing Workspace
1735    /// * `domain_id` - Domain UUID as string to remove
1736    ///
1737    /// # Returns
1738    ///
1739    /// JSON string containing updated Workspace, or JsValue error
1740    #[wasm_bindgen]
1741    pub fn remove_domain_from_workspace(
1742        workspace_json: &str,
1743        domain_id: &str,
1744    ) -> Result<String, JsValue> {
1745        use crate::models::workspace::Workspace;
1746        use chrono::Utc;
1747        use uuid::Uuid;
1748
1749        let mut workspace: Workspace =
1750            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1751        let domain_uuid =
1752            Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1753
1754        let original_len = workspace.domains.len();
1755        workspace.domains.retain(|d| d.id != domain_uuid);
1756
1757        if workspace.domains.len() == original_len {
1758            return Err(WasmError::new(
1759                "NotFoundError",
1760                format!("Domain {} not found in workspace", domain_id),
1761            )
1762            .with_code("DOMAIN_NOT_FOUND")
1763            .to_js_value());
1764        }
1765
1766        workspace.last_modified_at = Utc::now();
1767        serde_json::to_string(&workspace).map_err(serialization_error)
1768    }
1769
1770    /// Add a relationship to a workspace.
1771    ///
1772    /// # Arguments
1773    ///
1774    /// * `workspace_json` - JSON string containing Workspace
1775    /// * `relationship_json` - JSON string containing Relationship
1776    ///
1777    /// # Returns
1778    ///
1779    /// JSON string containing updated Workspace, or JsValue error
1780    #[wasm_bindgen]
1781    pub fn add_relationship_to_workspace(
1782        workspace_json: &str,
1783        relationship_json: &str,
1784    ) -> Result<String, JsValue> {
1785        use crate::models::Relationship;
1786        use crate::models::workspace::Workspace;
1787
1788        let mut workspace: Workspace =
1789            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1790        let relationship: Relationship =
1791            serde_json::from_str(relationship_json).map_err(deserialization_error)?;
1792
1793        // Check if relationship already exists
1794        if workspace
1795            .relationships
1796            .iter()
1797            .any(|r| r.id == relationship.id)
1798        {
1799            return Err(WasmError::new(
1800                "DuplicateError",
1801                format!(
1802                    "Relationship {} already exists in workspace",
1803                    relationship.id
1804                ),
1805            )
1806            .with_code("DUPLICATE_RELATIONSHIP")
1807            .to_js_value());
1808        }
1809
1810        workspace.add_relationship(relationship);
1811        serde_json::to_string(&workspace).map_err(serialization_error)
1812    }
1813
1814    /// Remove a relationship from a workspace.
1815    ///
1816    /// # Arguments
1817    ///
1818    /// * `workspace_json` - JSON string containing Workspace
1819    /// * `relationship_id` - Relationship UUID as string to remove
1820    ///
1821    /// # Returns
1822    ///
1823    /// JSON string containing updated Workspace, or JsValue error
1824    #[wasm_bindgen]
1825    pub fn remove_relationship_from_workspace(
1826        workspace_json: &str,
1827        relationship_id: &str,
1828    ) -> Result<String, JsValue> {
1829        use crate::models::workspace::Workspace;
1830        use uuid::Uuid;
1831
1832        let mut workspace: Workspace =
1833            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1834        let relationship_uuid = Uuid::parse_str(relationship_id)
1835            .map_err(|e| invalid_input_error("relationship ID", e))?;
1836
1837        if !workspace.remove_relationship(relationship_uuid) {
1838            return Err(WasmError::new(
1839                "NotFoundError",
1840                format!("Relationship {} not found in workspace", relationship_id),
1841            )
1842            .with_code("RELATIONSHIP_NOT_FOUND")
1843            .to_js_value());
1844        }
1845
1846        serde_json::to_string(&workspace).map_err(serialization_error)
1847    }
1848
1849    /// Get relationships for a source table from a workspace.
1850    ///
1851    /// # Arguments
1852    ///
1853    /// * `workspace_json` - JSON string containing Workspace
1854    /// * `source_table_id` - Source table UUID as string
1855    ///
1856    /// # Returns
1857    ///
1858    /// JSON string containing array of Relationships, or JsValue error
1859    #[wasm_bindgen]
1860    pub fn get_workspace_relationships_for_source(
1861        workspace_json: &str,
1862        source_table_id: &str,
1863    ) -> Result<String, JsValue> {
1864        use crate::models::workspace::Workspace;
1865        use uuid::Uuid;
1866
1867        let workspace: Workspace =
1868            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1869        let source_uuid = Uuid::parse_str(source_table_id)
1870            .map_err(|e| invalid_input_error("source table ID", e))?;
1871
1872        let relationships: Vec<_> = workspace.get_relationships_for_source(source_uuid);
1873        serde_json::to_string(&relationships).map_err(serialization_error)
1874    }
1875
1876    /// Get relationships for a target table from a workspace.
1877    ///
1878    /// # Arguments
1879    ///
1880    /// * `workspace_json` - JSON string containing Workspace
1881    /// * `target_table_id` - Target table UUID as string
1882    ///
1883    /// # Returns
1884    ///
1885    /// JSON string containing array of Relationships, or JsValue error
1886    #[wasm_bindgen]
1887    pub fn get_workspace_relationships_for_target(
1888        workspace_json: &str,
1889        target_table_id: &str,
1890    ) -> Result<String, JsValue> {
1891        use crate::models::workspace::Workspace;
1892        use uuid::Uuid;
1893
1894        let workspace: Workspace =
1895            serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1896        let target_uuid = Uuid::parse_str(target_table_id)
1897            .map_err(|e| invalid_input_error("target table ID", e))?;
1898
1899        let relationships: Vec<_> = workspace.get_relationships_for_target(target_uuid);
1900        serde_json::to_string(&relationships).map_err(serialization_error)
1901    }
1902
1903    /// Create a new domain configuration.
1904    ///
1905    /// # Arguments
1906    ///
1907    /// * `name` - Domain name
1908    /// * `workspace_id` - Workspace UUID as string
1909    ///
1910    /// # Returns
1911    ///
1912    /// JSON string containing DomainConfig, or JsValue error
1913    #[wasm_bindgen]
1914    pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1915        use crate::models::domain_config::DomainConfig;
1916        use chrono::Utc;
1917        use std::collections::HashMap;
1918        use uuid::Uuid;
1919
1920        let workspace_uuid =
1921            Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1922
1923        let config = DomainConfig {
1924            id: Uuid::new_v4(),
1925            workspace_id: workspace_uuid,
1926            name: name.to_string(),
1927            description: None,
1928            created_at: Utc::now(),
1929            last_modified_at: Utc::now(),
1930            owner: None,
1931            systems: Vec::new(),
1932            tables: Vec::new(),
1933            products: Vec::new(),
1934            assets: Vec::new(),
1935            processes: Vec::new(),
1936            decisions: Vec::new(),
1937            view_positions: HashMap::new(),
1938            folder_path: None,
1939            workspace_path: None,
1940        };
1941
1942        serde_json::to_string(&config).map_err(serialization_error)
1943    }
1944
1945    /// Parse domain config YAML content and return a structured representation.
1946    ///
1947    /// # Arguments
1948    ///
1949    /// * `yaml_content` - Domain config YAML content as a string
1950    ///
1951    /// # Returns
1952    ///
1953    /// JSON string containing DomainConfig, or JsValue error
1954    #[wasm_bindgen]
1955    pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1956        use crate::models::domain_config::DomainConfig;
1957
1958        let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1959        serde_json::to_string(&config).map_err(serialization_error)
1960    }
1961
1962    /// Export a domain config to YAML format.
1963    ///
1964    /// # Arguments
1965    ///
1966    /// * `config_json` - JSON string containing DomainConfig
1967    ///
1968    /// # Returns
1969    ///
1970    /// DomainConfig YAML format string, or JsValue error
1971    #[wasm_bindgen]
1972    pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1973        use crate::models::domain_config::DomainConfig;
1974
1975        let config: DomainConfig =
1976            serde_json::from_str(config_json).map_err(deserialization_error)?;
1977        serde_yaml::to_string(&config).map_err(serialization_error)
1978    }
1979
1980    /// Get the domain ID from a domain config JSON.
1981    ///
1982    /// # Arguments
1983    ///
1984    /// * `config_json` - JSON string containing DomainConfig
1985    ///
1986    /// # Returns
1987    ///
1988    /// Domain UUID as string, or JsValue error
1989    #[wasm_bindgen]
1990    pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1991        use crate::models::domain_config::DomainConfig;
1992
1993        let config: DomainConfig =
1994            serde_json::from_str(config_json).map_err(deserialization_error)?;
1995        Ok(config.id.to_string())
1996    }
1997
1998    /// Update domain config with new view positions.
1999    ///
2000    /// # Arguments
2001    ///
2002    /// * `config_json` - JSON string containing DomainConfig
2003    /// * `positions_json` - JSON string containing view positions map
2004    ///
2005    /// # Returns
2006    ///
2007    /// JSON string containing updated DomainConfig, or JsValue error
2008    #[wasm_bindgen]
2009    pub fn update_domain_view_positions(
2010        config_json: &str,
2011        positions_json: &str,
2012    ) -> Result<String, JsValue> {
2013        use crate::models::domain_config::{DomainConfig, ViewPosition};
2014        use chrono::Utc;
2015        use std::collections::HashMap;
2016
2017        let mut config: DomainConfig =
2018            serde_json::from_str(config_json).map_err(deserialization_error)?;
2019        let positions: HashMap<String, HashMap<String, ViewPosition>> =
2020            serde_json::from_str(positions_json).map_err(deserialization_error)?;
2021
2022        config.view_positions = positions;
2023        config.last_modified_at = Utc::now();
2024
2025        serde_json::to_string(&config).map_err(serialization_error)
2026    }
2027
2028    /// Add an entity reference to a domain config.
2029    ///
2030    /// # Arguments
2031    ///
2032    /// * `config_json` - JSON string containing DomainConfig
2033    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2034    /// * `entity_id` - Entity UUID as string
2035    ///
2036    /// # Returns
2037    ///
2038    /// JSON string containing updated DomainConfig, or JsValue error
2039    #[wasm_bindgen]
2040    pub fn add_entity_to_domain_config(
2041        config_json: &str,
2042        entity_type: &str,
2043        entity_id: &str,
2044    ) -> Result<String, JsValue> {
2045        use crate::models::domain_config::DomainConfig;
2046        use chrono::Utc;
2047        use uuid::Uuid;
2048
2049        let mut config: DomainConfig =
2050            serde_json::from_str(config_json).map_err(deserialization_error)?;
2051        let entity_uuid =
2052            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2053
2054        let entities = match entity_type {
2055            "system" => &mut config.systems,
2056            "table" => &mut config.tables,
2057            "product" => &mut config.products,
2058            "asset" => &mut config.assets,
2059            "process" => &mut config.processes,
2060            "decision" => &mut config.decisions,
2061            _ => {
2062                return Err(invalid_input_error(
2063                    "entity type",
2064                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2065                ));
2066            }
2067        };
2068
2069        if entities.contains(&entity_uuid) {
2070            return Err(WasmError::new(
2071                "DuplicateError",
2072                format!(
2073                    "{} {} already exists in domain config",
2074                    entity_type, entity_id
2075                ),
2076            )
2077            .with_code("DUPLICATE_ENTITY")
2078            .to_js_value());
2079        }
2080
2081        entities.push(entity_uuid);
2082        config.last_modified_at = Utc::now();
2083
2084        serde_json::to_string(&config).map_err(serialization_error)
2085    }
2086
2087    /// Remove an entity reference from a domain config.
2088    ///
2089    /// # Arguments
2090    ///
2091    /// * `config_json` - JSON string containing DomainConfig
2092    /// * `entity_type` - Entity type: "system", "table", "product", "asset", "process", "decision"
2093    /// * `entity_id` - Entity UUID as string to remove
2094    ///
2095    /// # Returns
2096    ///
2097    /// JSON string containing updated DomainConfig, or JsValue error
2098    #[wasm_bindgen]
2099    pub fn remove_entity_from_domain_config(
2100        config_json: &str,
2101        entity_type: &str,
2102        entity_id: &str,
2103    ) -> Result<String, JsValue> {
2104        use crate::models::domain_config::DomainConfig;
2105        use chrono::Utc;
2106        use uuid::Uuid;
2107
2108        let mut config: DomainConfig =
2109            serde_json::from_str(config_json).map_err(deserialization_error)?;
2110        let entity_uuid =
2111            Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2112
2113        let entities = match entity_type {
2114            "system" => &mut config.systems,
2115            "table" => &mut config.tables,
2116            "product" => &mut config.products,
2117            "asset" => &mut config.assets,
2118            "process" => &mut config.processes,
2119            "decision" => &mut config.decisions,
2120            _ => {
2121                return Err(invalid_input_error(
2122                    "entity type",
2123                    "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2124                ));
2125            }
2126        };
2127
2128        let original_len = entities.len();
2129        entities.retain(|id| *id != entity_uuid);
2130
2131        if entities.len() == original_len {
2132            return Err(WasmError::new(
2133                "NotFoundError",
2134                format!("{} {} not found in domain config", entity_type, entity_id),
2135            )
2136            .with_code("ENTITY_NOT_FOUND")
2137            .to_js_value());
2138        }
2139
2140        config.last_modified_at = Utc::now();
2141        serde_json::to_string(&config).map_err(serialization_error)
2142    }
2143
2144    // ============================================================================
2145    // Decision Log (DDL) Operations
2146    // ============================================================================
2147
2148    /// Parse a decision YAML file and return a structured representation.
2149    ///
2150    /// # Arguments
2151    ///
2152    /// * `yaml_content` - Decision YAML content as a string (.madr.yaml)
2153    ///
2154    /// # Returns
2155    ///
2156    /// JSON string containing Decision, or JsValue error
2157    #[wasm_bindgen]
2158    pub fn parse_decision_yaml(yaml_content: &str) -> Result<String, JsValue> {
2159        use crate::import::decision::DecisionImporter;
2160
2161        let importer = DecisionImporter::new();
2162        match importer.import(yaml_content) {
2163            Ok(decision) => serde_json::to_string(&decision).map_err(serialization_error),
2164            Err(e) => Err(import_error_to_js(e)),
2165        }
2166    }
2167
2168    /// Parse a decisions index YAML file and return a structured representation.
2169    ///
2170    /// # Arguments
2171    ///
2172    /// * `yaml_content` - Decisions index YAML content as a string (decisions.yaml)
2173    ///
2174    /// # Returns
2175    ///
2176    /// JSON string containing DecisionIndex, or JsValue error
2177    #[wasm_bindgen]
2178    pub fn parse_decision_index_yaml(yaml_content: &str) -> Result<String, JsValue> {
2179        use crate::import::decision::DecisionImporter;
2180
2181        let importer = DecisionImporter::new();
2182        match importer.import_index(yaml_content) {
2183            Ok(index) => serde_json::to_string(&index).map_err(serialization_error),
2184            Err(e) => Err(import_error_to_js(e)),
2185        }
2186    }
2187
2188    /// Export a decision to YAML format.
2189    ///
2190    /// # Arguments
2191    ///
2192    /// * `decision_json` - JSON string containing Decision
2193    ///
2194    /// # Returns
2195    ///
2196    /// Decision YAML format string, or JsValue error
2197    #[wasm_bindgen]
2198    pub fn export_decision_to_yaml(decision_json: &str) -> Result<String, JsValue> {
2199        use crate::export::decision::DecisionExporter;
2200        use crate::models::decision::Decision;
2201
2202        let decision: Decision =
2203            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2204        let exporter = DecisionExporter::new();
2205        exporter
2206            .export_without_validation(&decision)
2207            .map_err(export_error_to_js)
2208    }
2209
2210    /// Export a decisions index to YAML format.
2211    ///
2212    /// # Arguments
2213    ///
2214    /// * `index_json` - JSON string containing DecisionIndex
2215    ///
2216    /// # Returns
2217    ///
2218    /// DecisionIndex YAML format string, or JsValue error
2219    #[wasm_bindgen]
2220    pub fn export_decision_index_to_yaml(index_json: &str) -> Result<String, JsValue> {
2221        use crate::export::decision::DecisionExporter;
2222        use crate::models::decision::DecisionIndex;
2223
2224        let index: DecisionIndex =
2225            serde_json::from_str(index_json).map_err(deserialization_error)?;
2226        let exporter = DecisionExporter::new();
2227        exporter.export_index(&index).map_err(export_error_to_js)
2228    }
2229
2230    /// Export a decision to Markdown format (MADR template).
2231    ///
2232    /// # Arguments
2233    ///
2234    /// * `decision_json` - JSON string containing Decision
2235    ///
2236    /// # Returns
2237    ///
2238    /// Decision Markdown string, or JsValue error
2239    #[wasm_bindgen]
2240    pub fn export_decision_to_markdown(decision_json: &str) -> Result<String, JsValue> {
2241        use crate::export::markdown::MarkdownExporter;
2242        use crate::models::decision::Decision;
2243
2244        let decision: Decision =
2245            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2246        let exporter = MarkdownExporter::new();
2247        exporter
2248            .export_decision(&decision)
2249            .map_err(export_error_to_js)
2250    }
2251
2252    /// Create a new decision with required fields.
2253    ///
2254    /// # Arguments
2255    ///
2256    /// * `number` - Decision number (ADR-0001, ADR-0002, etc.)
2257    /// * `title` - Short title describing the decision
2258    /// * `context` - Problem statement and context
2259    /// * `decision` - The decision that was made
2260    ///
2261    /// # Returns
2262    ///
2263    /// JSON string containing Decision, or JsValue error
2264    #[wasm_bindgen]
2265    pub fn create_decision(
2266        number: u32,
2267        title: &str,
2268        context: &str,
2269        decision: &str,
2270    ) -> Result<String, JsValue> {
2271        use crate::models::decision::Decision;
2272
2273        let dec = Decision::new(number.into(), title, context, decision);
2274        serde_json::to_string(&dec).map_err(serialization_error)
2275    }
2276
2277    /// Create a new empty decision index.
2278    ///
2279    /// # Returns
2280    ///
2281    /// JSON string containing DecisionIndex, or JsValue error
2282    #[wasm_bindgen]
2283    pub fn create_decision_index() -> Result<String, JsValue> {
2284        use crate::models::decision::DecisionIndex;
2285
2286        let index = DecisionIndex::new();
2287        serde_json::to_string(&index).map_err(serialization_error)
2288    }
2289
2290    /// Add a decision to an index.
2291    ///
2292    /// # Arguments
2293    ///
2294    /// * `index_json` - JSON string containing DecisionIndex
2295    /// * `decision_json` - JSON string containing Decision
2296    /// * `filename` - Filename for the decision YAML file
2297    ///
2298    /// # Returns
2299    ///
2300    /// JSON string containing updated DecisionIndex, or JsValue error
2301    #[wasm_bindgen]
2302    pub fn add_decision_to_index(
2303        index_json: &str,
2304        decision_json: &str,
2305        filename: &str,
2306    ) -> Result<String, JsValue> {
2307        use crate::models::decision::{Decision, DecisionIndex};
2308
2309        let mut index: DecisionIndex =
2310            serde_json::from_str(index_json).map_err(deserialization_error)?;
2311        let decision: Decision =
2312            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2313
2314        index.add_decision(&decision, filename.to_string());
2315        serde_json::to_string(&index).map_err(serialization_error)
2316    }
2317
2318    // ============================================================================
2319    // Knowledge Base (KB) Operations
2320    // ============================================================================
2321
2322    /// Parse a knowledge article YAML file and return a structured representation.
2323    ///
2324    /// # Arguments
2325    ///
2326    /// * `yaml_content` - Knowledge article YAML content as a string (.kb.yaml)
2327    ///
2328    /// # Returns
2329    ///
2330    /// JSON string containing KnowledgeArticle, or JsValue error
2331    #[wasm_bindgen]
2332    pub fn parse_knowledge_yaml(yaml_content: &str) -> Result<String, JsValue> {
2333        use crate::import::knowledge::KnowledgeImporter;
2334
2335        let importer = KnowledgeImporter::new();
2336        match importer.import(yaml_content) {
2337            Ok(article) => serde_json::to_string(&article).map_err(serialization_error),
2338            Err(e) => Err(import_error_to_js(e)),
2339        }
2340    }
2341
2342    /// Parse a knowledge index YAML file and return a structured representation.
2343    ///
2344    /// # Arguments
2345    ///
2346    /// * `yaml_content` - Knowledge index YAML content as a string (knowledge.yaml)
2347    ///
2348    /// # Returns
2349    ///
2350    /// JSON string containing KnowledgeIndex, or JsValue error
2351    #[wasm_bindgen]
2352    pub fn parse_knowledge_index_yaml(yaml_content: &str) -> Result<String, JsValue> {
2353        use crate::import::knowledge::KnowledgeImporter;
2354
2355        let importer = KnowledgeImporter::new();
2356        match importer.import_index(yaml_content) {
2357            Ok(index) => serde_json::to_string(&index).map_err(serialization_error),
2358            Err(e) => Err(import_error_to_js(e)),
2359        }
2360    }
2361
2362    /// Export a knowledge article to YAML format.
2363    ///
2364    /// # Arguments
2365    ///
2366    /// * `article_json` - JSON string containing KnowledgeArticle
2367    ///
2368    /// # Returns
2369    ///
2370    /// KnowledgeArticle YAML format string, or JsValue error
2371    #[wasm_bindgen]
2372    pub fn export_knowledge_to_yaml(article_json: &str) -> Result<String, JsValue> {
2373        use crate::export::knowledge::KnowledgeExporter;
2374        use crate::models::knowledge::KnowledgeArticle;
2375
2376        let article: KnowledgeArticle =
2377            serde_json::from_str(article_json).map_err(deserialization_error)?;
2378        let exporter = KnowledgeExporter::new();
2379        exporter
2380            .export_without_validation(&article)
2381            .map_err(export_error_to_js)
2382    }
2383
2384    /// Export a knowledge index to YAML format.
2385    ///
2386    /// # Arguments
2387    ///
2388    /// * `index_json` - JSON string containing KnowledgeIndex
2389    ///
2390    /// # Returns
2391    ///
2392    /// KnowledgeIndex YAML format string, or JsValue error
2393    #[wasm_bindgen]
2394    pub fn export_knowledge_index_to_yaml(index_json: &str) -> Result<String, JsValue> {
2395        use crate::export::knowledge::KnowledgeExporter;
2396        use crate::models::knowledge::KnowledgeIndex;
2397
2398        let index: KnowledgeIndex =
2399            serde_json::from_str(index_json).map_err(deserialization_error)?;
2400        let exporter = KnowledgeExporter::new();
2401        exporter.export_index(&index).map_err(export_error_to_js)
2402    }
2403
2404    /// Export a knowledge article to Markdown format.
2405    ///
2406    /// # Arguments
2407    ///
2408    /// * `article_json` - JSON string containing KnowledgeArticle
2409    ///
2410    /// # Returns
2411    ///
2412    /// KnowledgeArticle Markdown string, or JsValue error
2413    #[wasm_bindgen]
2414    pub fn export_knowledge_to_markdown(article_json: &str) -> Result<String, JsValue> {
2415        use crate::export::markdown::MarkdownExporter;
2416        use crate::models::knowledge::KnowledgeArticle;
2417
2418        let article: KnowledgeArticle =
2419            serde_json::from_str(article_json).map_err(deserialization_error)?;
2420        let exporter = MarkdownExporter::new();
2421        exporter
2422            .export_knowledge(&article)
2423            .map_err(export_error_to_js)
2424    }
2425
2426    /// Create a new knowledge article with required fields.
2427    ///
2428    /// # Arguments
2429    ///
2430    /// * `number` - Article number (1, 2, 3, etc. - will be formatted as KB-0001)
2431    /// * `title` - Article title
2432    /// * `summary` - Brief summary of the article
2433    /// * `content` - Full article content in Markdown
2434    /// * `author` - Article author (email or name)
2435    ///
2436    /// # Returns
2437    ///
2438    /// JSON string containing KnowledgeArticle, or JsValue error
2439    #[wasm_bindgen]
2440    pub fn create_knowledge_article(
2441        number: u32,
2442        title: &str,
2443        summary: &str,
2444        content: &str,
2445        author: &str,
2446    ) -> Result<String, JsValue> {
2447        use crate::models::knowledge::KnowledgeArticle;
2448
2449        let article = KnowledgeArticle::new(number.into(), title, summary, content, author);
2450        serde_json::to_string(&article).map_err(serialization_error)
2451    }
2452
2453    /// Create a new empty knowledge index.
2454    ///
2455    /// # Returns
2456    ///
2457    /// JSON string containing KnowledgeIndex, or JsValue error
2458    #[wasm_bindgen]
2459    pub fn create_knowledge_index() -> Result<String, JsValue> {
2460        use crate::models::knowledge::KnowledgeIndex;
2461
2462        let index = KnowledgeIndex::new();
2463        serde_json::to_string(&index).map_err(serialization_error)
2464    }
2465
2466    /// Add an article to a knowledge index.
2467    ///
2468    /// # Arguments
2469    ///
2470    /// * `index_json` - JSON string containing KnowledgeIndex
2471    /// * `article_json` - JSON string containing KnowledgeArticle
2472    /// * `filename` - Filename for the article YAML file
2473    ///
2474    /// # Returns
2475    ///
2476    /// JSON string containing updated KnowledgeIndex, or JsValue error
2477    #[wasm_bindgen]
2478    pub fn add_article_to_knowledge_index(
2479        index_json: &str,
2480        article_json: &str,
2481        filename: &str,
2482    ) -> Result<String, JsValue> {
2483        use crate::models::knowledge::{KnowledgeArticle, KnowledgeIndex};
2484
2485        let mut index: KnowledgeIndex =
2486            serde_json::from_str(index_json).map_err(deserialization_error)?;
2487        let article: KnowledgeArticle =
2488            serde_json::from_str(article_json).map_err(deserialization_error)?;
2489
2490        index.add_article(&article, filename.to_string());
2491        serde_json::to_string(&index).map_err(serialization_error)
2492    }
2493
2494    /// Search knowledge articles by title, summary, or content.
2495    ///
2496    /// # Arguments
2497    ///
2498    /// * `articles_json` - JSON string containing array of KnowledgeArticle
2499    /// * `query` - Search query string (case-insensitive)
2500    ///
2501    /// # Returns
2502    ///
2503    /// JSON string containing array of matching KnowledgeArticle, or JsValue error
2504    #[wasm_bindgen]
2505    pub fn search_knowledge_articles(articles_json: &str, query: &str) -> Result<String, JsValue> {
2506        use crate::models::knowledge::KnowledgeArticle;
2507
2508        let articles: Vec<KnowledgeArticle> =
2509            serde_json::from_str(articles_json).map_err(deserialization_error)?;
2510
2511        let query_lower = query.to_lowercase();
2512        let matches: Vec<&KnowledgeArticle> = articles
2513            .iter()
2514            .filter(|article| {
2515                article.title.to_lowercase().contains(&query_lower)
2516                    || article.summary.to_lowercase().contains(&query_lower)
2517                    || article.content.to_lowercase().contains(&query_lower)
2518                    || article
2519                        .tags
2520                        .iter()
2521                        .any(|tag| tag.to_string().to_lowercase().contains(&query_lower))
2522            })
2523            .collect();
2524
2525        serde_json::to_string(&matches).map_err(serialization_error)
2526    }
2527
2528    // ==================== PDF Export Bindings ====================
2529
2530    /// Export a decision to PDF format with optional branding.
2531    ///
2532    /// # Arguments
2533    ///
2534    /// * `decision_json` - JSON string containing Decision
2535    /// * `branding_json` - Optional JSON string containing BrandingConfig
2536    ///
2537    /// # Returns
2538    ///
2539    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2540    #[wasm_bindgen]
2541    pub fn export_decision_to_pdf(
2542        decision_json: &str,
2543        branding_json: Option<String>,
2544    ) -> Result<String, JsValue> {
2545        use crate::export::pdf::{BrandingConfig, PdfExporter};
2546        use crate::models::decision::Decision;
2547
2548        let decision: Decision =
2549            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2550
2551        let exporter = if let Some(branding_str) = branding_json {
2552            let branding: BrandingConfig =
2553                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2554            PdfExporter::with_branding(branding)
2555        } else {
2556            PdfExporter::new()
2557        };
2558
2559        let result = exporter
2560            .export_decision(&decision)
2561            .map_err(export_error_to_js)?;
2562
2563        serde_json::to_string(&result).map_err(serialization_error)
2564    }
2565
2566    /// Export a knowledge article to PDF format with optional branding.
2567    ///
2568    /// # Arguments
2569    ///
2570    /// * `article_json` - JSON string containing KnowledgeArticle
2571    /// * `branding_json` - Optional JSON string containing BrandingConfig
2572    ///
2573    /// # Returns
2574    ///
2575    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2576    #[wasm_bindgen]
2577    pub fn export_knowledge_to_pdf(
2578        article_json: &str,
2579        branding_json: Option<String>,
2580    ) -> Result<String, JsValue> {
2581        use crate::export::pdf::{BrandingConfig, PdfExporter};
2582        use crate::models::knowledge::KnowledgeArticle;
2583
2584        let article: KnowledgeArticle =
2585            serde_json::from_str(article_json).map_err(deserialization_error)?;
2586
2587        let exporter = if let Some(branding_str) = branding_json {
2588            let branding: BrandingConfig =
2589                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2590            PdfExporter::with_branding(branding)
2591        } else {
2592            PdfExporter::new()
2593        };
2594
2595        let result = exporter
2596            .export_knowledge(&article)
2597            .map_err(export_error_to_js)?;
2598
2599        serde_json::to_string(&result).map_err(serialization_error)
2600    }
2601
2602    /// Export raw markdown content to PDF format with optional branding.
2603    ///
2604    /// # Arguments
2605    ///
2606    /// * `title` - Document title
2607    /// * `content` - Markdown content
2608    /// * `filename` - Output filename suggestion
2609    /// * `branding_json` - Optional JSON string containing BrandingConfig
2610    ///
2611    /// # Returns
2612    ///
2613    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2614    #[wasm_bindgen]
2615    pub fn export_markdown_to_pdf(
2616        title: &str,
2617        content: &str,
2618        filename: &str,
2619        branding_json: Option<String>,
2620    ) -> Result<String, JsValue> {
2621        use crate::export::pdf::{BrandingConfig, PdfExporter};
2622
2623        let exporter = if let Some(branding_str) = branding_json {
2624            let branding: BrandingConfig =
2625                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2626            PdfExporter::with_branding(branding)
2627        } else {
2628            PdfExporter::new()
2629        };
2630
2631        let result = exporter
2632            .export_markdown(title, content, filename)
2633            .map_err(export_error_to_js)?;
2634
2635        serde_json::to_string(&result).map_err(serialization_error)
2636    }
2637
2638    // ==================== Branded Markdown Export Bindings ====================
2639
2640    /// Export a decision to branded Markdown format.
2641    ///
2642    /// # Arguments
2643    ///
2644    /// * `decision_json` - JSON string containing Decision
2645    /// * `branding_json` - Optional JSON string containing MarkdownBrandingConfig
2646    ///
2647    /// # Returns
2648    ///
2649    /// Branded Markdown string, or JsValue error
2650    #[wasm_bindgen]
2651    pub fn export_decision_to_branded_markdown(
2652        decision_json: &str,
2653        branding_json: Option<String>,
2654    ) -> Result<String, JsValue> {
2655        use crate::export::markdown::{BrandedMarkdownExporter, MarkdownBrandingConfig};
2656        use crate::models::decision::Decision;
2657
2658        let decision: Decision =
2659            serde_json::from_str(decision_json).map_err(deserialization_error)?;
2660
2661        let exporter = if let Some(branding_str) = branding_json {
2662            let branding: MarkdownBrandingConfig =
2663                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2664            BrandedMarkdownExporter::with_branding(branding)
2665        } else {
2666            BrandedMarkdownExporter::new()
2667        };
2668
2669        exporter
2670            .export_decision(&decision)
2671            .map_err(export_error_to_js)
2672    }
2673
2674    /// Export a knowledge article to branded Markdown format.
2675    ///
2676    /// # Arguments
2677    ///
2678    /// * `article_json` - JSON string containing KnowledgeArticle
2679    /// * `branding_json` - Optional JSON string containing MarkdownBrandingConfig
2680    ///
2681    /// # Returns
2682    ///
2683    /// Branded Markdown string, or JsValue error
2684    #[wasm_bindgen]
2685    pub fn export_knowledge_to_branded_markdown(
2686        article_json: &str,
2687        branding_json: Option<String>,
2688    ) -> Result<String, JsValue> {
2689        use crate::export::markdown::{BrandedMarkdownExporter, MarkdownBrandingConfig};
2690        use crate::models::knowledge::KnowledgeArticle;
2691
2692        let article: KnowledgeArticle =
2693            serde_json::from_str(article_json).map_err(deserialization_error)?;
2694
2695        let exporter = if let Some(branding_str) = branding_json {
2696            let branding: MarkdownBrandingConfig =
2697                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2698            BrandedMarkdownExporter::with_branding(branding)
2699        } else {
2700            BrandedMarkdownExporter::new()
2701        };
2702
2703        exporter
2704            .export_knowledge(&article)
2705            .map_err(export_error_to_js)
2706    }
2707
2708    /// Get default PDF branding configuration.
2709    ///
2710    /// # Returns
2711    ///
2712    /// JSON string containing default BrandingConfig
2713    #[wasm_bindgen]
2714    pub fn get_default_pdf_branding() -> Result<String, JsValue> {
2715        use crate::export::pdf::BrandingConfig;
2716
2717        let config = BrandingConfig::default();
2718        serde_json::to_string(&config).map_err(serialization_error)
2719    }
2720
2721    /// Get default Markdown branding configuration.
2722    ///
2723    /// # Returns
2724    ///
2725    /// JSON string containing default MarkdownBrandingConfig
2726    #[wasm_bindgen]
2727    pub fn get_default_markdown_branding() -> Result<String, JsValue> {
2728        use crate::export::markdown::MarkdownBrandingConfig;
2729
2730        let config = MarkdownBrandingConfig::default();
2731        serde_json::to_string(&config).map_err(serialization_error)
2732    }
2733
2734    // ============================================================================
2735    // ODCS/ODPS/CADS PDF Export Bindings
2736    // ============================================================================
2737
2738    /// Export an ODCS Table (Data Contract) to PDF format with optional branding.
2739    ///
2740    /// # Arguments
2741    ///
2742    /// * `table_json` - JSON string containing Table
2743    /// * `branding_json` - Optional JSON string containing BrandingConfig
2744    ///
2745    /// # Returns
2746    ///
2747    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2748    #[wasm_bindgen]
2749    pub fn export_table_to_pdf(
2750        table_json: &str,
2751        branding_json: Option<String>,
2752    ) -> Result<String, JsValue> {
2753        use crate::export::pdf::{BrandingConfig, PdfExporter};
2754        use crate::models::Table;
2755
2756        let table: Table = serde_json::from_str(table_json).map_err(deserialization_error)?;
2757
2758        let exporter = if let Some(branding_str) = branding_json {
2759            let branding: BrandingConfig =
2760                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2761            PdfExporter::with_branding(branding)
2762        } else {
2763            PdfExporter::new()
2764        };
2765
2766        let result = exporter.export_table(&table).map_err(export_error_to_js)?;
2767
2768        serde_json::to_string(&result).map_err(serialization_error)
2769    }
2770
2771    /// Export an ODPS Data Product to PDF format with optional branding.
2772    ///
2773    /// # Arguments
2774    ///
2775    /// * `product_json` - JSON string containing ODPSDataProduct
2776    /// * `branding_json` - Optional JSON string containing BrandingConfig
2777    ///
2778    /// # Returns
2779    ///
2780    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2781    #[wasm_bindgen]
2782    pub fn export_odps_to_pdf(
2783        product_json: &str,
2784        branding_json: Option<String>,
2785    ) -> Result<String, JsValue> {
2786        use crate::export::pdf::{BrandingConfig, PdfExporter};
2787        use crate::models::odps::ODPSDataProduct;
2788
2789        let product: ODPSDataProduct =
2790            serde_json::from_str(product_json).map_err(deserialization_error)?;
2791
2792        let exporter = if let Some(branding_str) = branding_json {
2793            let branding: BrandingConfig =
2794                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2795            PdfExporter::with_branding(branding)
2796        } else {
2797            PdfExporter::new()
2798        };
2799
2800        let result = exporter
2801            .export_data_product(&product)
2802            .map_err(export_error_to_js)?;
2803
2804        serde_json::to_string(&result).map_err(serialization_error)
2805    }
2806
2807    /// Export a CADS Asset to PDF format with optional branding.
2808    ///
2809    /// # Arguments
2810    ///
2811    /// * `asset_json` - JSON string containing CADSAsset
2812    /// * `branding_json` - Optional JSON string containing BrandingConfig
2813    ///
2814    /// # Returns
2815    ///
2816    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2817    #[wasm_bindgen]
2818    pub fn export_cads_to_pdf(
2819        asset_json: &str,
2820        branding_json: Option<String>,
2821    ) -> Result<String, JsValue> {
2822        use crate::export::pdf::{BrandingConfig, PdfExporter};
2823        use crate::models::cads::CADSAsset;
2824
2825        let asset: CADSAsset = serde_json::from_str(asset_json).map_err(deserialization_error)?;
2826
2827        let exporter = if let Some(branding_str) = branding_json {
2828            let branding: BrandingConfig =
2829                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2830            PdfExporter::with_branding(branding)
2831        } else {
2832            PdfExporter::new()
2833        };
2834
2835        let result = exporter
2836            .export_cads_asset(&asset)
2837            .map_err(export_error_to_js)?;
2838
2839        serde_json::to_string(&result).map_err(serialization_error)
2840    }
2841
2842    /// Export an ODCS Table (Data Contract) to Markdown format.
2843    ///
2844    /// # Arguments
2845    ///
2846    /// * `table_json` - JSON string containing Table
2847    ///
2848    /// # Returns
2849    ///
2850    /// Markdown string, or JsValue error
2851    #[wasm_bindgen]
2852    pub fn export_table_to_markdown(table_json: &str) -> Result<String, JsValue> {
2853        use crate::export::pdf::PdfExporter;
2854        use crate::models::Table;
2855
2856        let table: Table = serde_json::from_str(table_json).map_err(deserialization_error)?;
2857
2858        // Use PdfExporter's internal markdown generation
2859        let exporter = PdfExporter::new();
2860        Ok(exporter.table_to_markdown_public(&table))
2861    }
2862
2863    /// Export an ODPS Data Product to Markdown format.
2864    ///
2865    /// # Arguments
2866    ///
2867    /// * `product_json` - JSON string containing ODPSDataProduct
2868    ///
2869    /// # Returns
2870    ///
2871    /// Markdown string, or JsValue error
2872    #[wasm_bindgen]
2873    pub fn export_odps_to_markdown(product_json: &str) -> Result<String, JsValue> {
2874        use crate::export::pdf::PdfExporter;
2875        use crate::models::odps::ODPSDataProduct;
2876
2877        let product: ODPSDataProduct =
2878            serde_json::from_str(product_json).map_err(deserialization_error)?;
2879
2880        let exporter = PdfExporter::new();
2881        Ok(exporter.data_product_to_markdown_public(&product))
2882    }
2883
2884    /// Export a CADS Asset to Markdown format.
2885    ///
2886    /// # Arguments
2887    ///
2888    /// * `asset_json` - JSON string containing CADSAsset
2889    ///
2890    /// # Returns
2891    ///
2892    /// Markdown string, or JsValue error
2893    #[wasm_bindgen]
2894    pub fn export_cads_to_markdown(asset_json: &str) -> Result<String, JsValue> {
2895        use crate::export::pdf::PdfExporter;
2896        use crate::models::cads::CADSAsset;
2897
2898        let asset: CADSAsset = serde_json::from_str(asset_json).map_err(deserialization_error)?;
2899
2900        let exporter = PdfExporter::new();
2901        Ok(exporter.cads_asset_to_markdown_public(&asset))
2902    }
2903
2904    // ============================================================================
2905    // Native Format PDF/Markdown Export (accepts YAML content directly)
2906    // ============================================================================
2907
2908    /// Export ODCS YAML content to PDF format with optional branding.
2909    ///
2910    /// Accepts raw ODCS YAML content (as you would find in an .odcs.yaml file)
2911    /// and exports it to PDF.
2912    ///
2913    /// # Arguments
2914    ///
2915    /// * `odcs_yaml` - ODCS YAML content as a string
2916    /// * `branding_json` - Optional JSON string containing BrandingConfig
2917    ///
2918    /// # Returns
2919    ///
2920    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
2921    #[wasm_bindgen]
2922    pub fn export_odcs_yaml_to_pdf(
2923        odcs_yaml: &str,
2924        branding_json: Option<String>,
2925    ) -> Result<String, JsValue> {
2926        use crate::export::pdf::{BrandingConfig, PdfExporter};
2927        use crate::import::ODCSImporter;
2928
2929        // Parse ODCS YAML to get tables
2930        let mut importer = ODCSImporter::new();
2931        let import_result = importer.import(odcs_yaml).map_err(import_error_to_js)?;
2932
2933        if import_result.tables.is_empty() {
2934            return Err(
2935                WasmError::new("ImportError", "ODCS content contains no tables")
2936                    .with_code("NO_TABLES")
2937                    .to_js_value(),
2938            );
2939        }
2940
2941        // Convert first table from import result to Table
2942        let table_data = &import_result.tables[0];
2943        let table = crate::models::Table::from_table_data(table_data);
2944
2945        let exporter = if let Some(branding_str) = branding_json {
2946            let branding: BrandingConfig =
2947                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
2948            PdfExporter::with_branding(branding)
2949        } else {
2950            PdfExporter::new()
2951        };
2952
2953        let result = exporter.export_table(&table).map_err(export_error_to_js)?;
2954        serde_json::to_string(&result).map_err(serialization_error)
2955    }
2956
2957    /// Export ODCS YAML content to Markdown format.
2958    ///
2959    /// Accepts raw ODCS YAML content and exports it to Markdown.
2960    ///
2961    /// # Arguments
2962    ///
2963    /// * `odcs_yaml` - ODCS YAML content as a string
2964    ///
2965    /// # Returns
2966    ///
2967    /// Markdown string, or JsValue error
2968    #[wasm_bindgen]
2969    pub fn export_odcs_yaml_to_markdown(odcs_yaml: &str) -> Result<String, JsValue> {
2970        use crate::export::pdf::PdfExporter;
2971        use crate::import::ODCSImporter;
2972
2973        let mut importer = ODCSImporter::new();
2974        let import_result = importer.import(odcs_yaml).map_err(import_error_to_js)?;
2975
2976        if import_result.tables.is_empty() {
2977            return Err(
2978                WasmError::new("ImportError", "ODCS content contains no tables")
2979                    .with_code("NO_TABLES")
2980                    .to_js_value(),
2981            );
2982        }
2983
2984        let table_data = &import_result.tables[0];
2985        let table = crate::models::Table::from_table_data(table_data);
2986
2987        let exporter = PdfExporter::new();
2988        Ok(exporter.table_to_markdown_public(&table))
2989    }
2990
2991    /// Export ODPS YAML content to PDF format with optional branding.
2992    ///
2993    /// Accepts raw ODPS YAML content (as you would find in an .odps.yaml file)
2994    /// and exports it to PDF.
2995    ///
2996    /// # Arguments
2997    ///
2998    /// * `odps_yaml` - ODPS YAML content as a string
2999    /// * `branding_json` - Optional JSON string containing BrandingConfig
3000    ///
3001    /// # Returns
3002    ///
3003    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
3004    #[wasm_bindgen]
3005    pub fn export_odps_yaml_to_pdf(
3006        odps_yaml: &str,
3007        branding_json: Option<String>,
3008    ) -> Result<String, JsValue> {
3009        use crate::export::pdf::{BrandingConfig, PdfExporter};
3010        use crate::import::ODPSImporter;
3011
3012        let importer = ODPSImporter::new();
3013        let product = importer.import(odps_yaml).map_err(import_error_to_js)?;
3014
3015        let exporter = if let Some(branding_str) = branding_json {
3016            let branding: BrandingConfig =
3017                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
3018            PdfExporter::with_branding(branding)
3019        } else {
3020            PdfExporter::new()
3021        };
3022
3023        let result = exporter
3024            .export_data_product(&product)
3025            .map_err(export_error_to_js)?;
3026        serde_json::to_string(&result).map_err(serialization_error)
3027    }
3028
3029    /// Export ODPS YAML content to Markdown format.
3030    ///
3031    /// Accepts raw ODPS YAML content and exports it to Markdown.
3032    ///
3033    /// # Arguments
3034    ///
3035    /// * `odps_yaml` - ODPS YAML content as a string
3036    ///
3037    /// # Returns
3038    ///
3039    /// Markdown string, or JsValue error
3040    #[wasm_bindgen]
3041    pub fn export_odps_yaml_to_markdown(odps_yaml: &str) -> Result<String, JsValue> {
3042        use crate::export::pdf::PdfExporter;
3043        use crate::import::ODPSImporter;
3044
3045        let importer = ODPSImporter::new();
3046        let product = importer.import(odps_yaml).map_err(import_error_to_js)?;
3047
3048        let exporter = PdfExporter::new();
3049        Ok(exporter.data_product_to_markdown_public(&product))
3050    }
3051
3052    /// Export CADS YAML content to PDF format with optional branding.
3053    ///
3054    /// Accepts raw CADS YAML content (as you would find in a .cads.yaml file)
3055    /// and exports it to PDF.
3056    ///
3057    /// # Arguments
3058    ///
3059    /// * `cads_yaml` - CADS YAML content as a string
3060    /// * `branding_json` - Optional JSON string containing BrandingConfig
3061    ///
3062    /// # Returns
3063    ///
3064    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
3065    #[wasm_bindgen]
3066    pub fn export_cads_yaml_to_pdf(
3067        cads_yaml: &str,
3068        branding_json: Option<String>,
3069    ) -> Result<String, JsValue> {
3070        use crate::export::pdf::{BrandingConfig, PdfExporter};
3071        use crate::import::CADSImporter;
3072
3073        let importer = CADSImporter::new();
3074        let asset = importer.import(cads_yaml).map_err(import_error_to_js)?;
3075
3076        let exporter = if let Some(branding_str) = branding_json {
3077            let branding: BrandingConfig =
3078                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
3079            PdfExporter::with_branding(branding)
3080        } else {
3081            PdfExporter::new()
3082        };
3083
3084        let result = exporter
3085            .export_cads_asset(&asset)
3086            .map_err(export_error_to_js)?;
3087        serde_json::to_string(&result).map_err(serialization_error)
3088    }
3089
3090    /// Export CADS YAML content to Markdown format.
3091    ///
3092    /// Accepts raw CADS YAML content and exports it to Markdown.
3093    ///
3094    /// # Arguments
3095    ///
3096    /// * `cads_yaml` - CADS YAML content as a string
3097    ///
3098    /// # Returns
3099    ///
3100    /// Markdown string, or JsValue error
3101    #[wasm_bindgen]
3102    pub fn export_cads_yaml_to_markdown(cads_yaml: &str) -> Result<String, JsValue> {
3103        use crate::export::pdf::PdfExporter;
3104        use crate::import::CADSImporter;
3105
3106        let importer = CADSImporter::new();
3107        let asset = importer.import(cads_yaml).map_err(import_error_to_js)?;
3108
3109        let exporter = PdfExporter::new();
3110        Ok(exporter.cads_asset_to_markdown_public(&asset))
3111    }
3112
3113    /// Export Decision YAML content to PDF format with optional branding.
3114    ///
3115    /// Accepts raw Decision YAML content (as you would find in a .madr.yaml file)
3116    /// and exports it to PDF.
3117    ///
3118    /// # Arguments
3119    ///
3120    /// * `decision_yaml` - Decision YAML content as a string
3121    /// * `branding_json` - Optional JSON string containing BrandingConfig
3122    ///
3123    /// # Returns
3124    ///
3125    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
3126    #[wasm_bindgen]
3127    pub fn export_decision_yaml_to_pdf(
3128        decision_yaml: &str,
3129        branding_json: Option<String>,
3130    ) -> Result<String, JsValue> {
3131        use crate::export::pdf::{BrandingConfig, PdfExporter};
3132        use crate::import::decision::DecisionImporter;
3133
3134        let importer = DecisionImporter::new();
3135        let decision = importer
3136            .import_without_validation(decision_yaml)
3137            .map_err(import_error_to_js)?;
3138
3139        let exporter = if let Some(branding_str) = branding_json {
3140            let branding: BrandingConfig =
3141                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
3142            PdfExporter::with_branding(branding)
3143        } else {
3144            PdfExporter::new()
3145        };
3146
3147        let result = exporter
3148            .export_decision(&decision)
3149            .map_err(export_error_to_js)?;
3150        serde_json::to_string(&result).map_err(serialization_error)
3151    }
3152
3153    /// Export Decision YAML content to Markdown format.
3154    ///
3155    /// Accepts raw Decision YAML content and exports it to Markdown.
3156    ///
3157    /// # Arguments
3158    ///
3159    /// * `decision_yaml` - Decision YAML content as a string
3160    ///
3161    /// # Returns
3162    ///
3163    /// Markdown string, or JsValue error
3164    #[wasm_bindgen]
3165    pub fn export_decision_yaml_to_markdown(decision_yaml: &str) -> Result<String, JsValue> {
3166        use crate::export::markdown::MarkdownExporter;
3167        use crate::import::decision::DecisionImporter;
3168
3169        let importer = DecisionImporter::new();
3170        let decision = importer
3171            .import_without_validation(decision_yaml)
3172            .map_err(import_error_to_js)?;
3173
3174        let exporter = MarkdownExporter::new();
3175        exporter
3176            .export_decision(&decision)
3177            .map_err(export_error_to_js)
3178    }
3179
3180    /// Export Knowledge Article YAML content to PDF format with optional branding.
3181    ///
3182    /// Accepts raw Knowledge Article YAML content (as you would find in a .kb.yaml file)
3183    /// and exports it to PDF.
3184    ///
3185    /// # Arguments
3186    ///
3187    /// * `knowledge_yaml` - Knowledge Article YAML content as a string
3188    /// * `branding_json` - Optional JSON string containing BrandingConfig
3189    ///
3190    /// # Returns
3191    ///
3192    /// JSON string containing PdfExportResult (with base64-encoded PDF), or JsValue error
3193    #[wasm_bindgen]
3194    pub fn export_knowledge_yaml_to_pdf(
3195        knowledge_yaml: &str,
3196        branding_json: Option<String>,
3197    ) -> Result<String, JsValue> {
3198        use crate::export::pdf::{BrandingConfig, PdfExporter};
3199        use crate::import::knowledge::KnowledgeImporter;
3200
3201        let importer = KnowledgeImporter::new();
3202        let article = importer
3203            .import_without_validation(knowledge_yaml)
3204            .map_err(import_error_to_js)?;
3205
3206        let exporter = if let Some(branding_str) = branding_json {
3207            let branding: BrandingConfig =
3208                serde_json::from_str(&branding_str).map_err(deserialization_error)?;
3209            PdfExporter::with_branding(branding)
3210        } else {
3211            PdfExporter::new()
3212        };
3213
3214        let result = exporter
3215            .export_knowledge(&article)
3216            .map_err(export_error_to_js)?;
3217        serde_json::to_string(&result).map_err(serialization_error)
3218    }
3219
3220    /// Export Knowledge Article YAML content to Markdown format.
3221    ///
3222    /// Accepts raw Knowledge Article YAML content and exports it to Markdown.
3223    ///
3224    /// # Arguments
3225    ///
3226    /// * `knowledge_yaml` - Knowledge Article YAML content as a string
3227    ///
3228    /// # Returns
3229    ///
3230    /// Markdown string, or JsValue error
3231    #[wasm_bindgen]
3232    pub fn export_knowledge_yaml_to_markdown(knowledge_yaml: &str) -> Result<String, JsValue> {
3233        use crate::export::markdown::MarkdownExporter;
3234        use crate::import::knowledge::KnowledgeImporter;
3235
3236        let importer = KnowledgeImporter::new();
3237        let article = importer
3238            .import_without_validation(knowledge_yaml)
3239            .map_err(import_error_to_js)?;
3240
3241        let exporter = MarkdownExporter::new();
3242        exporter
3243            .export_knowledge(&article)
3244            .map_err(export_error_to_js)
3245    }
3246}