1pub mod auth;
12#[cfg(feature = "cli")]
13pub mod cli;
14pub mod convert;
15#[cfg(feature = "database")]
16pub mod database;
17pub mod export;
18#[cfg(feature = "git")]
19pub mod git;
20pub mod import;
21pub mod model;
22pub mod models;
23pub mod storage;
24pub mod validation;
25pub mod workspace;
26
27#[cfg(feature = "api-backend")]
29pub use storage::api::ApiStorageBackend;
30#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
31pub use storage::browser::BrowserStorageBackend;
32#[cfg(feature = "native-fs")]
33pub use storage::filesystem::FileSystemStorageBackend;
34pub use storage::{StorageBackend, StorageError};
35
36pub use convert::{ConversionError, convert_to_odcs};
37#[cfg(feature = "png-export")]
38pub use export::PNGExporter;
39pub use export::{
40 AvroExporter, ExportError, ExportResult, JSONSchemaExporter, ODCSExporter, ProtobufExporter,
41 SQLExporter,
42};
43pub use import::{
44 AvroImporter, ImportError, ImportResult, JSONSchemaImporter, ODCSImporter, ProtobufImporter,
45 SQLImporter,
46};
47#[cfg(feature = "api-backend")]
48pub use model::ApiModelLoader;
49pub use model::{ModelLoader, ModelSaver};
50pub use validation::{
51 RelationshipValidationError, RelationshipValidationResult, TableValidationError,
52 TableValidationResult,
53};
54
55pub use models::enums::*;
57pub use models::{Column, ContactDetails, DataModel, ForeignKey, Relationship, SlaProperty, Table};
58
59pub use auth::{
61 AuthMode, AuthState, GitHubEmail, InitiateOAuthRequest, InitiateOAuthResponse,
62 SelectEmailRequest,
63};
64
65pub use workspace::{
67 CreateWorkspaceRequest, CreateWorkspaceResponse, ListProfilesResponse, LoadProfileRequest,
68 ProfileInfo, WorkspaceInfo,
69};
70
71#[cfg(feature = "git")]
73pub use git::{GitError, GitService, GitStatus};
74
75#[cfg(all(target_arch = "wasm32", feature = "wasm"))]
77mod wasm {
78 use crate::export::ExportError;
79 use crate::import::{ImportError, ImportResult};
80 use crate::models::DataModel;
81 use js_sys;
82 use serde::{Deserialize, Serialize};
83 use serde_json;
84 use serde_yaml;
85 use uuid;
86 use wasm_bindgen::prelude::*;
87 use wasm_bindgen_futures;
88
89 #[derive(Debug, Clone, Serialize, Deserialize)]
92 pub struct WasmError {
93 pub error_type: String,
95 pub message: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub code: Option<String>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub details: Option<serde_json::Value>,
103 }
104
105 impl WasmError {
106 fn new(error_type: impl Into<String>, message: impl Into<String>) -> Self {
108 Self {
109 error_type: error_type.into(),
110 message: message.into(),
111 code: None,
112 details: None,
113 }
114 }
115
116 fn with_code(mut self, code: impl Into<String>) -> Self {
118 self.code = Some(code.into());
119 self
120 }
121
122 fn to_js_value(&self) -> JsValue {
124 match serde_json::to_string(self) {
126 Ok(json) => JsValue::from_str(&json),
127 Err(_) => JsValue::from_str(&self.message),
129 }
130 }
131 }
132
133 fn import_error_to_js(err: ImportError) -> JsValue {
135 WasmError::new("ImportError", err.to_string())
136 .with_code("IMPORT_FAILED")
137 .to_js_value()
138 }
139
140 fn export_error_to_js(err: ExportError) -> JsValue {
142 WasmError::new("ExportError", err.to_string())
143 .with_code("EXPORT_FAILED")
144 .to_js_value()
145 }
146
147 fn serialization_error(err: impl std::fmt::Display) -> JsValue {
149 WasmError::new(
150 "SerializationError",
151 format!("Serialization error: {}", err),
152 )
153 .with_code("SERIALIZATION_FAILED")
154 .to_js_value()
155 }
156
157 fn deserialization_error(err: impl std::fmt::Display) -> JsValue {
159 WasmError::new(
160 "DeserializationError",
161 format!("Deserialization error: {}", err),
162 )
163 .with_code("DESERIALIZATION_FAILED")
164 .to_js_value()
165 }
166
167 fn parse_error(err: impl std::fmt::Display) -> JsValue {
169 WasmError::new("ParseError", format!("Parse error: {}", err))
170 .with_code("PARSE_FAILED")
171 .to_js_value()
172 }
173
174 fn validation_error(err: impl std::fmt::Display) -> JsValue {
176 WasmError::new("ValidationError", err.to_string())
177 .with_code("VALIDATION_FAILED")
178 .to_js_value()
179 }
180
181 fn invalid_input_error(field: &str, err: impl std::fmt::Display) -> JsValue {
183 WasmError::new("InvalidInputError", format!("Invalid {}: {}", field, err))
184 .with_code("INVALID_INPUT")
185 .to_js_value()
186 }
187
188 fn conversion_error(err: impl std::fmt::Display) -> JsValue {
190 WasmError::new("ConversionError", format!("Conversion error: {}", err))
191 .with_code("CONVERSION_FAILED")
192 .to_js_value()
193 }
194
195 fn storage_error(err: impl std::fmt::Display) -> JsValue {
197 WasmError::new("StorageError", format!("Storage error: {}", err))
198 .with_code("STORAGE_FAILED")
199 .to_js_value()
200 }
201
202 fn serialize_import_result(result: &ImportResult) -> Result<String, JsValue> {
204 serde_json::to_string(result).map_err(serialization_error)
205 }
206
207 fn flatten_struct_columns(result: ImportResult) -> ImportResult {
215 use crate::import::{ColumnData, ODCSImporter, TableData};
216
217 let importer = ODCSImporter::new();
218
219 let tables = result
220 .tables
221 .into_iter()
222 .map(|table_data| {
223 let mut all_columns = Vec::new();
224
225 for col_data in table_data.columns {
226 let data_type_upper = col_data.data_type.to_uppercase();
227 let is_map = data_type_upper.starts_with("MAP<");
228
229 if is_map {
231 all_columns.push(col_data);
232 continue;
233 }
234
235 let is_struct = data_type_upper.contains("STRUCT<");
237 if is_struct {
238 let field_data = serde_json::Map::new();
239 if let Ok(nested_cols) = importer.parse_struct_type_from_string(
240 &col_data.name,
241 &col_data.data_type,
242 &field_data,
243 ) {
244 if !nested_cols.is_empty() {
245 let parent_data_type =
247 if col_data.data_type.to_uppercase().starts_with("ARRAY<") {
248 "ARRAY<STRUCT<...>>".to_string()
249 } else {
250 "STRUCT<...>".to_string()
251 };
252
253 all_columns.push(ColumnData {
254 name: col_data.name.clone(),
255 data_type: parent_data_type,
256 physical_type: col_data.physical_type.clone(),
257 nullable: col_data.nullable,
258 primary_key: col_data.primary_key,
259 description: col_data.description.clone(),
260 quality: col_data.quality.clone(),
261 relationships: col_data.relationships.clone(),
262 enum_values: col_data.enum_values.clone(),
263 ..Default::default()
264 });
265
266 for nested_col in nested_cols {
268 all_columns.push(
269 crate::import::odcs_shared::column_to_column_data(
270 &nested_col,
271 ),
272 );
273 }
274 continue;
275 }
276 }
277 }
278
279 all_columns.push(col_data);
281 }
282
283 TableData {
284 table_index: table_data.table_index,
285 name: table_data.name,
286 columns: all_columns,
287 }
288 })
289 .collect();
290
291 ImportResult {
292 tables,
293 tables_requiring_name: result.tables_requiring_name,
294 errors: result.errors,
295 ai_suggestions: result.ai_suggestions,
296 }
297 }
298
299 fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
301 serde_json::from_str(json).map_err(deserialization_error)
302 }
303
304 #[wasm_bindgen]
314 pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
315 let mut importer = crate::import::ODCSImporter::new();
316 match importer.import(yaml_content) {
317 Ok(result) => {
318 let flattened = flatten_struct_columns(result);
319 serialize_import_result(&flattened)
320 }
321 Err(err) => Err(import_error_to_js(err)),
322 }
323 }
324
325 #[wasm_bindgen]
341 pub fn parse_odcl_yaml(yaml_content: &str) -> Result<String, JsValue> {
342 let mut importer = crate::import::ODCLImporter::new();
343 match importer.import(yaml_content) {
344 Ok(result) => {
345 let flattened = flatten_struct_columns(result);
346 serialize_import_result(&flattened)
347 }
348 Err(err) => Err(import_error_to_js(err)),
349 }
350 }
351
352 #[wasm_bindgen]
365 pub fn is_odcl_format(yaml_content: &str) -> bool {
366 let importer = crate::import::ODCLImporter::new();
367 importer.can_handle(yaml_content)
368 }
369
370 #[wasm_bindgen]
380 pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
381 let model = deserialize_workspace(workspace_json)?;
382
383 let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
385
386 let yaml_docs: Vec<String> = exports.values().cloned().collect();
388 Ok(yaml_docs.join("\n---\n"))
389 }
390
391 #[wasm_bindgen]
402 pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
403 let importer = crate::import::SQLImporter::new(dialect);
404 match importer.parse(sql_content) {
405 Ok(result) => {
406 let flattened = flatten_struct_columns(result);
408 serialize_import_result(&flattened)
409 }
410 Err(err) => Err(parse_error(err)),
411 }
412 }
413
414 #[wasm_bindgen]
424 pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
425 let importer = crate::import::AvroImporter::new();
426 match importer.import(avro_content) {
427 Ok(result) => {
428 let flattened = flatten_struct_columns(result);
429 serialize_import_result(&flattened)
430 }
431 Err(err) => Err(import_error_to_js(err)),
432 }
433 }
434
435 #[wasm_bindgen]
445 pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
446 let importer = crate::import::JSONSchemaImporter::new();
447 match importer.import(json_schema_content) {
448 Ok(result) => {
449 let flattened = flatten_struct_columns(result);
450 serialize_import_result(&flattened)
451 }
452 Err(err) => Err(import_error_to_js(err)),
453 }
454 }
455
456 #[wasm_bindgen]
466 pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
467 let importer = crate::import::ProtobufImporter::new();
468 match importer.import(protobuf_content) {
469 Ok(result) => {
470 let flattened = flatten_struct_columns(result);
471 serialize_import_result(&flattened)
472 }
473 Err(err) => Err(import_error_to_js(err)),
474 }
475 }
476
477 #[wasm_bindgen]
488 pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
489 let model = deserialize_workspace(workspace_json)?;
490 let exporter = crate::export::SQLExporter;
491 match exporter.export(&model.tables, Some(dialect)) {
492 Ok(result) => Ok(result.content),
493 Err(err) => Err(export_error_to_js(err)),
494 }
495 }
496
497 #[wasm_bindgen]
507 pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
508 let model = deserialize_workspace(workspace_json)?;
509 let exporter = crate::export::AvroExporter;
510 match exporter.export(&model.tables) {
511 Ok(result) => Ok(result.content),
512 Err(err) => Err(export_error_to_js(err)),
513 }
514 }
515
516 #[wasm_bindgen]
526 pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
527 let model = deserialize_workspace(workspace_json)?;
528 let exporter = crate::export::JSONSchemaExporter;
529 match exporter.export(&model.tables) {
530 Ok(result) => Ok(result.content),
531 Err(err) => Err(export_error_to_js(err)),
532 }
533 }
534
535 #[wasm_bindgen]
545 pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
546 let model = deserialize_workspace(workspace_json)?;
547 let exporter = crate::export::ProtobufExporter;
548 match exporter.export(&model.tables) {
549 Ok(result) => Ok(result.content),
550 Err(err) => Err(export_error_to_js(err)),
551 }
552 }
553
554 #[wasm_bindgen]
564 pub fn import_from_cads(yaml_content: &str) -> Result<String, JsValue> {
565 let importer = crate::import::CADSImporter::new();
566 match importer.import(yaml_content) {
567 Ok(asset) => serde_json::to_string(&asset).map_err(serialization_error),
568 Err(err) => Err(import_error_to_js(err)),
569 }
570 }
571
572 #[wasm_bindgen]
582 pub fn export_to_cads(asset_json: &str) -> Result<String, JsValue> {
583 let asset: crate::models::cads::CADSAsset =
584 serde_json::from_str(asset_json).map_err(deserialization_error)?;
585 let exporter = crate::export::CADSExporter;
586 match exporter.export(&asset) {
587 Ok(yaml) => Ok(yaml),
588 Err(err) => Err(export_error_to_js(err)),
589 }
590 }
591
592 #[wasm_bindgen]
602 pub fn import_from_odps(yaml_content: &str) -> Result<String, JsValue> {
603 let importer = crate::import::ODPSImporter::new();
604 match importer.import(yaml_content) {
605 Ok(product) => serde_json::to_string(&product).map_err(serialization_error),
606 Err(err) => Err(import_error_to_js(err)),
607 }
608 }
609
610 #[wasm_bindgen]
620 pub fn export_to_odps(product_json: &str) -> Result<String, JsValue> {
621 let product: crate::models::odps::ODPSDataProduct =
622 serde_json::from_str(product_json).map_err(deserialization_error)?;
623 let exporter = crate::export::ODPSExporter;
624 match exporter.export(&product) {
625 Ok(yaml) => Ok(yaml),
626 Err(err) => Err(export_error_to_js(err)),
627 }
628 }
629
630 #[cfg(feature = "odps-validation")]
640 #[wasm_bindgen]
641 pub fn validate_odps(yaml_content: &str) -> Result<(), JsValue> {
642 #[cfg(feature = "cli")]
643 {
644 use crate::cli::validation::validate_odps_internal;
645 validate_odps_internal(yaml_content).map_err(validation_error)
646 }
647 #[cfg(not(feature = "cli"))]
648 {
649 use jsonschema::Validator;
651 use serde_json::Value;
652
653 let schema_content = include_str!("../schemas/odps-json-schema-latest.json");
654 let schema: Value = serde_json::from_str(schema_content)
655 .map_err(|e| validation_error(format!("Failed to load ODPS schema: {}", e)))?;
656
657 let validator = Validator::new(&schema)
658 .map_err(|e| validation_error(format!("Failed to compile ODPS schema: {}", e)))?;
659
660 let data: Value = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
661
662 if let Err(error) = validator.validate(&data) {
663 return Err(validation_error(format!(
664 "ODPS validation failed: {}",
665 error
666 )));
667 }
668
669 Ok(())
670 }
671 }
672
673 #[cfg(not(feature = "odps-validation"))]
674 #[wasm_bindgen]
675 pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
676 Ok(())
679 }
680
681 #[wasm_bindgen]
691 pub fn create_domain(name: &str) -> Result<String, JsValue> {
692 let domain = crate::models::domain::Domain::new(name.to_string());
693 serde_json::to_string(&domain).map_err(serialization_error)
694 }
695
696 #[wasm_bindgen]
706 pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
707 match crate::models::domain::Domain::from_yaml(yaml_content) {
708 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
709 Err(e) => Err(parse_error(e)),
710 }
711 }
712
713 #[wasm_bindgen]
723 pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
724 let domain: crate::models::domain::Domain =
725 serde_json::from_str(domain_json).map_err(deserialization_error)?;
726 domain.to_yaml().map_err(serialization_error)
727 }
728
729 #[wasm_bindgen]
740 pub fn migrate_dataflow_to_domain(
741 dataflow_yaml: &str,
742 domain_name: Option<String>,
743 ) -> Result<String, JsValue> {
744 match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
745 dataflow_yaml,
746 domain_name.as_deref(),
747 ) {
748 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
749 Err(e) => Err(conversion_error(e)),
750 }
751 }
752
753 #[wasm_bindgen]
763 pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
764 use crate::models::Tag;
765 use std::str::FromStr;
766 match Tag::from_str(tag_str) {
767 Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
768 Err(_) => Err(parse_error("Invalid tag format")),
769 }
770 }
771
772 #[wasm_bindgen]
782 pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
783 use crate::models::Tag;
784 let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
785 Ok(tag.to_string())
786 }
787
788 #[wasm_bindgen]
800 pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
801 match crate::convert::convert_to_odcs(input, format.as_deref()) {
802 Ok(yaml) => Ok(yaml),
803 Err(e) => Err(conversion_error(e)),
804 }
805 }
806
807 #[wasm_bindgen]
818 pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
819 let model = deserialize_workspace(workspace_json)?;
820 let filtered = model.filter_nodes_by_owner(owner);
821 serde_json::to_string(&filtered).map_err(serialization_error)
822 }
823
824 #[wasm_bindgen]
835 pub fn filter_relationships_by_owner(
836 workspace_json: &str,
837 owner: &str,
838 ) -> Result<String, JsValue> {
839 let model = deserialize_workspace(workspace_json)?;
840 let filtered = model.filter_relationships_by_owner(owner);
841 serde_json::to_string(&filtered).map_err(serialization_error)
842 }
843
844 #[wasm_bindgen]
855 pub fn filter_nodes_by_infrastructure_type(
856 workspace_json: &str,
857 infrastructure_type: &str,
858 ) -> Result<String, JsValue> {
859 let model = deserialize_workspace(workspace_json)?;
860 let infra_type: crate::models::enums::InfrastructureType =
861 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
862 .map_err(|e| invalid_input_error("infrastructure type", e))?;
863 let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
864 serde_json::to_string(&filtered).map_err(serialization_error)
865 }
866
867 #[wasm_bindgen]
878 pub fn filter_relationships_by_infrastructure_type(
879 workspace_json: &str,
880 infrastructure_type: &str,
881 ) -> Result<String, JsValue> {
882 let model = deserialize_workspace(workspace_json)?;
883 let infra_type: crate::models::enums::InfrastructureType =
884 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
885 .map_err(|e| invalid_input_error("infrastructure type", e))?;
886 let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
887 serde_json::to_string(&filtered).map_err(serialization_error)
888 }
889
890 #[wasm_bindgen]
901 pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
902 let model = deserialize_workspace(workspace_json)?;
903 let (nodes, relationships) = model.filter_by_tags(tag);
904 let result = serde_json::json!({
905 "nodes": nodes,
906 "relationships": relationships
907 });
908 serde_json::to_string(&result).map_err(serialization_error)
909 }
910
911 #[wasm_bindgen]
927 pub fn add_system_to_domain(
928 workspace_json: &str,
929 domain_id: &str,
930 system_json: &str,
931 ) -> Result<String, JsValue> {
932 let mut model = deserialize_workspace(workspace_json)?;
933 let domain_uuid =
934 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
935 let system: crate::models::domain::System =
936 serde_json::from_str(system_json).map_err(deserialization_error)?;
937 model
938 .add_system_to_domain(domain_uuid, system)
939 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
940 serde_json::to_string(&model).map_err(serialization_error)
941 }
942
943 #[wasm_bindgen]
955 pub fn add_cads_node_to_domain(
956 workspace_json: &str,
957 domain_id: &str,
958 node_json: &str,
959 ) -> Result<String, JsValue> {
960 let mut model = deserialize_workspace(workspace_json)?;
961 let domain_uuid =
962 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
963 let node: crate::models::domain::CADSNode =
964 serde_json::from_str(node_json).map_err(deserialization_error)?;
965 model
966 .add_cads_node_to_domain(domain_uuid, node)
967 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
968 serde_json::to_string(&model).map_err(serialization_error)
969 }
970
971 #[wasm_bindgen]
983 pub fn add_odcs_node_to_domain(
984 workspace_json: &str,
985 domain_id: &str,
986 node_json: &str,
987 ) -> Result<String, JsValue> {
988 let mut model = deserialize_workspace(workspace_json)?;
989 let domain_uuid =
990 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
991 let node: crate::models::domain::ODCSNode =
992 serde_json::from_str(node_json).map_err(deserialization_error)?;
993 model
994 .add_odcs_node_to_domain(domain_uuid, node)
995 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
996 serde_json::to_string(&model).map_err(serialization_error)
997 }
998
999 #[wasm_bindgen]
1013 pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
1014 match crate::validation::input::validate_table_name(name) {
1015 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1016 Err(err) => {
1017 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1018 }
1019 }
1020 }
1021
1022 #[wasm_bindgen]
1032 pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
1033 match crate::validation::input::validate_column_name(name) {
1034 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1035 Err(err) => {
1036 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1037 }
1038 }
1039 }
1040
1041 #[wasm_bindgen]
1051 pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
1052 match crate::validation::input::validate_uuid(id) {
1053 Ok(uuid) => {
1054 Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
1055 }
1056 Err(err) => {
1057 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1058 }
1059 }
1060 }
1061
1062 #[wasm_bindgen]
1072 pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
1073 match crate::validation::input::validate_data_type(data_type) {
1074 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1075 Err(err) => {
1076 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1077 }
1078 }
1079 }
1080
1081 #[wasm_bindgen]
1091 pub fn validate_description(desc: &str) -> Result<String, JsValue> {
1092 match crate::validation::input::validate_description(desc) {
1093 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1094 Err(err) => {
1095 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1096 }
1097 }
1098 }
1099
1100 #[wasm_bindgen]
1111 pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
1112 crate::validation::input::sanitize_sql_identifier(name, dialect)
1113 }
1114
1115 #[wasm_bindgen]
1125 pub fn sanitize_description(desc: &str) -> String {
1126 crate::validation::input::sanitize_description(desc)
1127 }
1128
1129 #[wasm_bindgen]
1140 pub fn detect_naming_conflicts(
1141 existing_tables_json: &str,
1142 new_tables_json: &str,
1143 ) -> Result<String, JsValue> {
1144 let existing_tables: Vec<crate::models::Table> =
1145 serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1146 let new_tables: Vec<crate::models::Table> =
1147 serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1148
1149 let validator = crate::validation::tables::TableValidator::new();
1150 let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1151
1152 serde_json::to_string(&conflicts).map_err(serialization_error)
1153 }
1154
1155 #[wasm_bindgen]
1165 pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1166 let table: crate::models::Table =
1167 serde_json::from_str(table_json).map_err(deserialization_error)?;
1168
1169 let validator = crate::validation::tables::TableValidator::new();
1170 match validator.validate_pattern_exclusivity(&table) {
1171 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1172 Err(violation) => {
1173 Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1174 }
1175 }
1176 }
1177
1178 #[wasm_bindgen]
1190 pub fn check_circular_dependency(
1191 relationships_json: &str,
1192 source_table_id: &str,
1193 target_table_id: &str,
1194 ) -> Result<String, JsValue> {
1195 let relationships: Vec<crate::models::Relationship> =
1196 serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1197
1198 let source_id = uuid::Uuid::parse_str(source_table_id)
1199 .map_err(|e| invalid_input_error("source_table_id", e))?;
1200 let target_id = uuid::Uuid::parse_str(target_table_id)
1201 .map_err(|e| invalid_input_error("target_table_id", e))?;
1202
1203 let validator = crate::validation::relationships::RelationshipValidator::new();
1204 match validator.check_circular_dependency(&relationships, source_id, target_id) {
1205 Ok((has_cycle, cycle_path)) => {
1206 let cycle_path_strs: Vec<String> = cycle_path
1207 .map(|path| path.iter().map(|id| id.to_string()).collect())
1208 .unwrap_or_default();
1209 Ok(serde_json::json!({
1210 "has_cycle": has_cycle,
1211 "cycle_path": cycle_path_strs
1212 })
1213 .to_string())
1214 }
1215 Err(err) => Err(validation_error(err)),
1216 }
1217 }
1218
1219 #[wasm_bindgen]
1230 pub fn validate_no_self_reference(
1231 source_table_id: &str,
1232 target_table_id: &str,
1233 ) -> Result<String, JsValue> {
1234 let source_id = uuid::Uuid::parse_str(source_table_id)
1235 .map_err(|e| invalid_input_error("source_table_id", e))?;
1236 let target_id = uuid::Uuid::parse_str(target_table_id)
1237 .map_err(|e| invalid_input_error("target_table_id", e))?;
1238
1239 let validator = crate::validation::relationships::RelationshipValidator::new();
1240 match validator.validate_no_self_reference(source_id, target_id) {
1241 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1242 Err(self_ref) => {
1243 Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1244 }
1245 }
1246 }
1247
1248 #[cfg(feature = "png-export")]
1264 #[wasm_bindgen]
1265 pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1266 let model = deserialize_workspace(workspace_json)?;
1267 let exporter = crate::export::PNGExporter::new();
1268 match exporter.export(&model.tables, width, height) {
1269 Ok(result) => Ok(result.content), Err(err) => Err(export_error_to_js(err)),
1271 }
1272 }
1273
1274 #[wasm_bindgen]
1290 pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1291 let db_name = db_name.to_string();
1292 let store_name = store_name.to_string();
1293 let workspace_path = workspace_path.to_string();
1294
1295 wasm_bindgen_futures::future_to_promise(async move {
1296 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1297 let loader = crate::model::ModelLoader::new(storage);
1298 match loader.load_model(&workspace_path).await {
1299 Ok(result) => serde_json::to_string(&result)
1300 .map(|s| JsValue::from_str(&s))
1301 .map_err(serialization_error),
1302 Err(err) => Err(storage_error(err)),
1303 }
1304 })
1305 }
1306
1307 #[wasm_bindgen]
1320 pub fn save_model(
1321 db_name: &str,
1322 store_name: &str,
1323 workspace_path: &str,
1324 model_json: &str,
1325 ) -> js_sys::Promise {
1326 let db_name = db_name.to_string();
1327 let store_name = store_name.to_string();
1328 let workspace_path = workspace_path.to_string();
1329 let model_json = model_json.to_string();
1330
1331 wasm_bindgen_futures::future_to_promise(async move {
1332 let model: crate::models::DataModel =
1333 serde_json::from_str(&model_json).map_err(deserialization_error)?;
1334
1335 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1336 let saver = crate::model::ModelSaver::new(storage);
1337
1338 for table in &model.tables {
1341 let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1343 let table_data = crate::model::saver::TableData {
1344 id: table.id,
1345 name: table.name.clone(),
1346 yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1347 yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1348 };
1349 saver
1350 .save_table(&workspace_path, &table_data)
1351 .await
1352 .map_err(storage_error)?;
1353 }
1354
1355 if !model.relationships.is_empty() {
1357 let rel_data: Vec<crate::model::saver::RelationshipData> = model
1358 .relationships
1359 .iter()
1360 .map(|rel| {
1361 let yaml_value = serde_json::json!({
1362 "id": rel.id.to_string(),
1363 "source_table_id": rel.source_table_id.to_string(),
1364 "target_table_id": rel.target_table_id.to_string(),
1365 });
1366 let yaml_str = serde_json::to_string(&yaml_value)
1368 .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1369 let yaml_value = serde_yaml::from_str(&yaml_str)
1370 .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1371 Ok(crate::model::saver::RelationshipData {
1372 id: rel.id,
1373 source_table_id: rel.source_table_id,
1374 target_table_id: rel.target_table_id,
1375 yaml_value,
1376 })
1377 })
1378 .collect::<Result<Vec<_>, String>>()
1379 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1380
1381 saver
1382 .save_relationships(&workspace_path, &rel_data)
1383 .await
1384 .map_err(|e| storage_error(e))?;
1385 }
1386
1387 Ok(JsValue::from_str("Model saved successfully"))
1388 })
1389 }
1390
1391 #[cfg(feature = "bpmn")]
1404 #[wasm_bindgen]
1405 pub fn import_bpmn_model(
1406 domain_id: &str,
1407 xml_content: &str,
1408 model_name: Option<String>,
1409 ) -> Result<String, JsValue> {
1410 use crate::import::bpmn::BPMNImporter;
1411 use uuid::Uuid;
1412
1413 let domain_uuid =
1414 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1415
1416 let mut importer = BPMNImporter::new();
1417 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1418 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1419 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1420 }
1421 }
1422
1423 #[cfg(feature = "bpmn")]
1433 #[wasm_bindgen]
1434 pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1435 use crate::export::bpmn::BPMNExporter;
1436 let exporter = BPMNExporter::new();
1437 exporter
1438 .export(xml_content)
1439 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1440 }
1441
1442 #[cfg(feature = "dmn")]
1455 #[wasm_bindgen]
1456 pub fn import_dmn_model(
1457 domain_id: &str,
1458 xml_content: &str,
1459 model_name: Option<String>,
1460 ) -> Result<String, JsValue> {
1461 use crate::import::dmn::DMNImporter;
1462 use uuid::Uuid;
1463
1464 let domain_uuid =
1465 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1466
1467 let mut importer = DMNImporter::new();
1468 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1469 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1470 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1471 }
1472 }
1473
1474 #[cfg(feature = "dmn")]
1484 #[wasm_bindgen]
1485 pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1486 use crate::export::dmn::DMNExporter;
1487 let exporter = DMNExporter::new();
1488 exporter
1489 .export(xml_content)
1490 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1491 }
1492
1493 #[cfg(feature = "openapi")]
1506 #[wasm_bindgen]
1507 pub fn import_openapi_spec(
1508 domain_id: &str,
1509 content: &str,
1510 api_name: Option<String>,
1511 ) -> Result<String, JsValue> {
1512 use crate::import::openapi::OpenAPIImporter;
1513 use uuid::Uuid;
1514
1515 let domain_uuid =
1516 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1517
1518 let mut importer = OpenAPIImporter::new();
1519 match importer.import(content, domain_uuid, api_name.as_deref()) {
1520 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1521 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1522 }
1523 }
1524
1525 #[cfg(feature = "openapi")]
1537 #[wasm_bindgen]
1538 pub fn export_openapi_spec(
1539 content: &str,
1540 source_format: &str,
1541 target_format: Option<String>,
1542 ) -> Result<String, JsValue> {
1543 use crate::export::openapi::OpenAPIExporter;
1544 use crate::models::openapi::OpenAPIFormat;
1545
1546 let source_fmt = match source_format {
1547 "yaml" | "yml" => OpenAPIFormat::Yaml,
1548 "json" => OpenAPIFormat::Json,
1549 _ => {
1550 return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1551 }
1552 };
1553
1554 let target_fmt = if let Some(tf) = target_format {
1555 match tf.as_str() {
1556 "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1557 "json" => Some(OpenAPIFormat::Json),
1558 _ => {
1559 return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1560 }
1561 }
1562 } else {
1563 None
1564 };
1565
1566 let exporter = OpenAPIExporter::new();
1567 exporter
1568 .export(content, source_fmt, target_fmt)
1569 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1570 }
1571
1572 #[cfg(feature = "openapi")]
1584 #[wasm_bindgen]
1585 pub fn convert_openapi_to_odcs(
1586 openapi_content: &str,
1587 component_name: &str,
1588 table_name: Option<String>,
1589 ) -> Result<String, JsValue> {
1590 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1591
1592 let converter = OpenAPIToODCSConverter::new();
1593 match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1594 Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1595 Err(e) => Err(conversion_error(e)),
1596 }
1597 }
1598
1599 #[cfg(feature = "openapi")]
1610 #[wasm_bindgen]
1611 pub fn analyze_openapi_conversion(
1612 openapi_content: &str,
1613 component_name: &str,
1614 ) -> Result<String, JsValue> {
1615 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1616
1617 let converter = OpenAPIToODCSConverter::new();
1618 match converter.analyze_conversion(openapi_content, component_name) {
1619 Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1620 Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1621 .with_code("ANALYSIS_FAILED")
1622 .to_js_value()),
1623 }
1624 }
1625
1626 #[wasm_bindgen]
1641 pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1642 use crate::models::workspace::Workspace;
1643 use chrono::Utc;
1644 use uuid::Uuid;
1645
1646 let owner_uuid =
1647 Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1648
1649 let workspace = Workspace::new(name.to_string(), owner_uuid);
1650
1651 serde_json::to_string(&workspace).map_err(serialization_error)
1652 }
1653
1654 #[wasm_bindgen]
1664 pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1665 use crate::models::workspace::Workspace;
1666
1667 let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1668 serde_json::to_string(&workspace).map_err(serialization_error)
1669 }
1670
1671 #[wasm_bindgen]
1681 pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1682 use crate::models::workspace::Workspace;
1683
1684 let workspace: Workspace =
1685 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1686 serde_yaml::to_string(&workspace).map_err(serialization_error)
1687 }
1688
1689 #[wasm_bindgen]
1701 pub fn add_domain_to_workspace(
1702 workspace_json: &str,
1703 domain_id: &str,
1704 domain_name: &str,
1705 ) -> Result<String, JsValue> {
1706 use crate::models::workspace::{DomainReference, Workspace};
1707 use chrono::Utc;
1708 use uuid::Uuid;
1709
1710 let mut workspace: Workspace =
1711 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1712 let domain_uuid =
1713 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1714
1715 if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1717 return Err(WasmError::new(
1718 "DuplicateError",
1719 format!("Domain {} already exists in workspace", domain_id),
1720 )
1721 .with_code("DUPLICATE_DOMAIN")
1722 .to_js_value());
1723 }
1724
1725 workspace.domains.push(DomainReference {
1726 id: domain_uuid,
1727 name: domain_name.to_string(),
1728 description: None,
1729 systems: Vec::new(),
1730 });
1731 workspace.last_modified_at = Utc::now();
1732
1733 serde_json::to_string(&workspace).map_err(serialization_error)
1734 }
1735
1736 #[wasm_bindgen]
1747 pub fn remove_domain_from_workspace(
1748 workspace_json: &str,
1749 domain_id: &str,
1750 ) -> Result<String, JsValue> {
1751 use crate::models::workspace::Workspace;
1752 use chrono::Utc;
1753 use uuid::Uuid;
1754
1755 let mut workspace: Workspace =
1756 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1757 let domain_uuid =
1758 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1759
1760 let original_len = workspace.domains.len();
1761 workspace.domains.retain(|d| d.id != domain_uuid);
1762
1763 if workspace.domains.len() == original_len {
1764 return Err(WasmError::new(
1765 "NotFoundError",
1766 format!("Domain {} not found in workspace", domain_id),
1767 )
1768 .with_code("DOMAIN_NOT_FOUND")
1769 .to_js_value());
1770 }
1771
1772 workspace.last_modified_at = Utc::now();
1773 serde_json::to_string(&workspace).map_err(serialization_error)
1774 }
1775
1776 #[wasm_bindgen]
1787 pub fn add_relationship_to_workspace(
1788 workspace_json: &str,
1789 relationship_json: &str,
1790 ) -> Result<String, JsValue> {
1791 use crate::models::Relationship;
1792 use crate::models::workspace::Workspace;
1793
1794 let mut workspace: Workspace =
1795 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1796 let relationship: Relationship =
1797 serde_json::from_str(relationship_json).map_err(deserialization_error)?;
1798
1799 if workspace
1801 .relationships
1802 .iter()
1803 .any(|r| r.id == relationship.id)
1804 {
1805 return Err(WasmError::new(
1806 "DuplicateError",
1807 format!(
1808 "Relationship {} already exists in workspace",
1809 relationship.id
1810 ),
1811 )
1812 .with_code("DUPLICATE_RELATIONSHIP")
1813 .to_js_value());
1814 }
1815
1816 workspace.add_relationship(relationship);
1817 serde_json::to_string(&workspace).map_err(serialization_error)
1818 }
1819
1820 #[wasm_bindgen]
1831 pub fn remove_relationship_from_workspace(
1832 workspace_json: &str,
1833 relationship_id: &str,
1834 ) -> Result<String, JsValue> {
1835 use crate::models::workspace::Workspace;
1836 use uuid::Uuid;
1837
1838 let mut workspace: Workspace =
1839 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1840 let relationship_uuid = Uuid::parse_str(relationship_id)
1841 .map_err(|e| invalid_input_error("relationship ID", e))?;
1842
1843 if !workspace.remove_relationship(relationship_uuid) {
1844 return Err(WasmError::new(
1845 "NotFoundError",
1846 format!("Relationship {} not found in workspace", relationship_id),
1847 )
1848 .with_code("RELATIONSHIP_NOT_FOUND")
1849 .to_js_value());
1850 }
1851
1852 serde_json::to_string(&workspace).map_err(serialization_error)
1853 }
1854
1855 #[wasm_bindgen]
1866 pub fn get_workspace_relationships_for_source(
1867 workspace_json: &str,
1868 source_table_id: &str,
1869 ) -> Result<String, JsValue> {
1870 use crate::models::workspace::Workspace;
1871 use uuid::Uuid;
1872
1873 let workspace: Workspace =
1874 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1875 let source_uuid = Uuid::parse_str(source_table_id)
1876 .map_err(|e| invalid_input_error("source table ID", e))?;
1877
1878 let relationships: Vec<_> = workspace.get_relationships_for_source(source_uuid);
1879 serde_json::to_string(&relationships).map_err(serialization_error)
1880 }
1881
1882 #[wasm_bindgen]
1893 pub fn get_workspace_relationships_for_target(
1894 workspace_json: &str,
1895 target_table_id: &str,
1896 ) -> Result<String, JsValue> {
1897 use crate::models::workspace::Workspace;
1898 use uuid::Uuid;
1899
1900 let workspace: Workspace =
1901 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1902 let target_uuid = Uuid::parse_str(target_table_id)
1903 .map_err(|e| invalid_input_error("target table ID", e))?;
1904
1905 let relationships: Vec<_> = workspace.get_relationships_for_target(target_uuid);
1906 serde_json::to_string(&relationships).map_err(serialization_error)
1907 }
1908
1909 #[wasm_bindgen]
1920 pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1921 use crate::models::domain_config::DomainConfig;
1922 use chrono::Utc;
1923 use std::collections::HashMap;
1924 use uuid::Uuid;
1925
1926 let workspace_uuid =
1927 Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1928
1929 let config = DomainConfig {
1930 id: Uuid::new_v4(),
1931 workspace_id: workspace_uuid,
1932 name: name.to_string(),
1933 description: None,
1934 created_at: Utc::now(),
1935 last_modified_at: Utc::now(),
1936 owner: None,
1937 systems: Vec::new(),
1938 tables: Vec::new(),
1939 products: Vec::new(),
1940 assets: Vec::new(),
1941 processes: Vec::new(),
1942 decisions: Vec::new(),
1943 view_positions: HashMap::new(),
1944 folder_path: None,
1945 workspace_path: None,
1946 };
1947
1948 serde_json::to_string(&config).map_err(serialization_error)
1949 }
1950
1951 #[wasm_bindgen]
1961 pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1962 use crate::models::domain_config::DomainConfig;
1963
1964 let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1965 serde_json::to_string(&config).map_err(serialization_error)
1966 }
1967
1968 #[wasm_bindgen]
1978 pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1979 use crate::models::domain_config::DomainConfig;
1980
1981 let config: DomainConfig =
1982 serde_json::from_str(config_json).map_err(deserialization_error)?;
1983 serde_yaml::to_string(&config).map_err(serialization_error)
1984 }
1985
1986 #[wasm_bindgen]
1996 pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1997 use crate::models::domain_config::DomainConfig;
1998
1999 let config: DomainConfig =
2000 serde_json::from_str(config_json).map_err(deserialization_error)?;
2001 Ok(config.id.to_string())
2002 }
2003
2004 #[wasm_bindgen]
2015 pub fn update_domain_view_positions(
2016 config_json: &str,
2017 positions_json: &str,
2018 ) -> Result<String, JsValue> {
2019 use crate::models::domain_config::{DomainConfig, ViewPosition};
2020 use chrono::Utc;
2021 use std::collections::HashMap;
2022
2023 let mut config: DomainConfig =
2024 serde_json::from_str(config_json).map_err(deserialization_error)?;
2025 let positions: HashMap<String, HashMap<String, ViewPosition>> =
2026 serde_json::from_str(positions_json).map_err(deserialization_error)?;
2027
2028 config.view_positions = positions;
2029 config.last_modified_at = Utc::now();
2030
2031 serde_json::to_string(&config).map_err(serialization_error)
2032 }
2033
2034 #[wasm_bindgen]
2046 pub fn add_entity_to_domain_config(
2047 config_json: &str,
2048 entity_type: &str,
2049 entity_id: &str,
2050 ) -> Result<String, JsValue> {
2051 use crate::models::domain_config::DomainConfig;
2052 use chrono::Utc;
2053 use uuid::Uuid;
2054
2055 let mut config: DomainConfig =
2056 serde_json::from_str(config_json).map_err(deserialization_error)?;
2057 let entity_uuid =
2058 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2059
2060 let entities = match entity_type {
2061 "system" => &mut config.systems,
2062 "table" => &mut config.tables,
2063 "product" => &mut config.products,
2064 "asset" => &mut config.assets,
2065 "process" => &mut config.processes,
2066 "decision" => &mut config.decisions,
2067 _ => {
2068 return Err(invalid_input_error(
2069 "entity type",
2070 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2071 ));
2072 }
2073 };
2074
2075 if entities.contains(&entity_uuid) {
2076 return Err(WasmError::new(
2077 "DuplicateError",
2078 format!(
2079 "{} {} already exists in domain config",
2080 entity_type, entity_id
2081 ),
2082 )
2083 .with_code("DUPLICATE_ENTITY")
2084 .to_js_value());
2085 }
2086
2087 entities.push(entity_uuid);
2088 config.last_modified_at = Utc::now();
2089
2090 serde_json::to_string(&config).map_err(serialization_error)
2091 }
2092
2093 #[wasm_bindgen]
2105 pub fn remove_entity_from_domain_config(
2106 config_json: &str,
2107 entity_type: &str,
2108 entity_id: &str,
2109 ) -> Result<String, JsValue> {
2110 use crate::models::domain_config::DomainConfig;
2111 use chrono::Utc;
2112 use uuid::Uuid;
2113
2114 let mut config: DomainConfig =
2115 serde_json::from_str(config_json).map_err(deserialization_error)?;
2116 let entity_uuid =
2117 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2118
2119 let entities = match entity_type {
2120 "system" => &mut config.systems,
2121 "table" => &mut config.tables,
2122 "product" => &mut config.products,
2123 "asset" => &mut config.assets,
2124 "process" => &mut config.processes,
2125 "decision" => &mut config.decisions,
2126 _ => {
2127 return Err(invalid_input_error(
2128 "entity type",
2129 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2130 ));
2131 }
2132 };
2133
2134 let original_len = entities.len();
2135 entities.retain(|id| *id != entity_uuid);
2136
2137 if entities.len() == original_len {
2138 return Err(WasmError::new(
2139 "NotFoundError",
2140 format!("{} {} not found in domain config", entity_type, entity_id),
2141 )
2142 .with_code("ENTITY_NOT_FOUND")
2143 .to_js_value());
2144 }
2145
2146 config.last_modified_at = Utc::now();
2147 serde_json::to_string(&config).map_err(serialization_error)
2148 }
2149}