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