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