1pub 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#[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
53pub use models::enums::*;
55pub use models::{Column, ContactDetails, DataModel, ForeignKey, Relationship, SlaProperty, Table};
56
57pub use auth::{
59 AuthMode, AuthState, GitHubEmail, InitiateOAuthRequest, InitiateOAuthResponse,
60 SelectEmailRequest,
61};
62
63pub use workspace::{
65 CreateWorkspaceRequest, CreateWorkspaceResponse, ListProfilesResponse, LoadProfileRequest,
66 ProfileInfo, WorkspaceInfo,
67};
68
69#[cfg(feature = "git")]
71pub use git::{GitError, GitService, GitStatus};
72
73#[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 #[derive(Debug, Clone, Serialize, Deserialize)]
90 pub struct WasmError {
91 pub error_type: String,
93 pub message: String,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub code: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub details: Option<serde_json::Value>,
101 }
102
103 impl WasmError {
104 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 fn with_code(mut self, code: impl Into<String>) -> Self {
116 self.code = Some(code.into());
117 self
118 }
119
120 fn to_js_value(&self) -> JsValue {
122 match serde_json::to_string(self) {
124 Ok(json) => JsValue::from_str(&json),
125 Err(_) => JsValue::from_str(&self.message),
127 }
128 }
129 }
130
131 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 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 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 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 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 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 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 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 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 fn serialize_import_result(result: &ImportResult) -> Result<String, JsValue> {
202 serde_json::to_string(result).map_err(serialization_error)
203 }
204
205 fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
207 serde_json::from_str(json).map_err(deserialization_error)
208 }
209
210 #[wasm_bindgen]
220 pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
221 let mut importer = crate::import::ODCSImporter::new();
222 match importer.import(yaml_content) {
223 Ok(result) => serialize_import_result(&result),
224 Err(err) => Err(import_error_to_js(err)),
225 }
226 }
227
228 #[wasm_bindgen]
244 pub fn parse_odcl_yaml(yaml_content: &str) -> Result<String, JsValue> {
245 let mut importer = crate::import::ODCLImporter::new();
246 match importer.import(yaml_content) {
247 Ok(result) => serialize_import_result(&result),
248 Err(err) => Err(import_error_to_js(err)),
249 }
250 }
251
252 #[wasm_bindgen]
265 pub fn is_odcl_format(yaml_content: &str) -> bool {
266 let importer = crate::import::ODCLImporter::new();
267 importer.can_handle(yaml_content)
268 }
269
270 #[wasm_bindgen]
280 pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
281 let model = deserialize_workspace(workspace_json)?;
282
283 let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
285
286 let yaml_docs: Vec<String> = exports.values().cloned().collect();
288 Ok(yaml_docs.join("\n---\n"))
289 }
290
291 #[wasm_bindgen]
302 pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
303 let importer = crate::import::SQLImporter::new(dialect);
304 match importer.parse(sql_content) {
305 Ok(result) => serialize_import_result(&result),
306 Err(err) => Err(parse_error(err)),
307 }
308 }
309
310 #[wasm_bindgen]
320 pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
321 let importer = crate::import::AvroImporter::new();
322 match importer.import(avro_content) {
323 Ok(result) => serialize_import_result(&result),
324 Err(err) => Err(import_error_to_js(err)),
325 }
326 }
327
328 #[wasm_bindgen]
338 pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
339 let importer = crate::import::JSONSchemaImporter::new();
340 match importer.import(json_schema_content) {
341 Ok(result) => serialize_import_result(&result),
342 Err(err) => Err(import_error_to_js(err)),
343 }
344 }
345
346 #[wasm_bindgen]
356 pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
357 let importer = crate::import::ProtobufImporter::new();
358 match importer.import(protobuf_content) {
359 Ok(result) => serialize_import_result(&result),
360 Err(err) => Err(import_error_to_js(err)),
361 }
362 }
363
364 #[wasm_bindgen]
375 pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
376 let model = deserialize_workspace(workspace_json)?;
377 let exporter = crate::export::SQLExporter;
378 match exporter.export(&model.tables, Some(dialect)) {
379 Ok(result) => Ok(result.content),
380 Err(err) => Err(export_error_to_js(err)),
381 }
382 }
383
384 #[wasm_bindgen]
394 pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
395 let model = deserialize_workspace(workspace_json)?;
396 let exporter = crate::export::AvroExporter;
397 match exporter.export(&model.tables) {
398 Ok(result) => Ok(result.content),
399 Err(err) => Err(export_error_to_js(err)),
400 }
401 }
402
403 #[wasm_bindgen]
413 pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
414 let model = deserialize_workspace(workspace_json)?;
415 let exporter = crate::export::JSONSchemaExporter;
416 match exporter.export(&model.tables) {
417 Ok(result) => Ok(result.content),
418 Err(err) => Err(export_error_to_js(err)),
419 }
420 }
421
422 #[wasm_bindgen]
432 pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
433 let model = deserialize_workspace(workspace_json)?;
434 let exporter = crate::export::ProtobufExporter;
435 match exporter.export(&model.tables) {
436 Ok(result) => Ok(result.content),
437 Err(err) => Err(export_error_to_js(err)),
438 }
439 }
440
441 #[wasm_bindgen]
451 pub fn import_from_cads(yaml_content: &str) -> Result<String, JsValue> {
452 let importer = crate::import::CADSImporter::new();
453 match importer.import(yaml_content) {
454 Ok(asset) => serde_json::to_string(&asset).map_err(serialization_error),
455 Err(err) => Err(import_error_to_js(err)),
456 }
457 }
458
459 #[wasm_bindgen]
469 pub fn export_to_cads(asset_json: &str) -> Result<String, JsValue> {
470 let asset: crate::models::cads::CADSAsset =
471 serde_json::from_str(asset_json).map_err(deserialization_error)?;
472 let exporter = crate::export::CADSExporter;
473 match exporter.export(&asset) {
474 Ok(yaml) => Ok(yaml),
475 Err(err) => Err(export_error_to_js(err)),
476 }
477 }
478
479 #[wasm_bindgen]
489 pub fn import_from_odps(yaml_content: &str) -> Result<String, JsValue> {
490 let importer = crate::import::ODPSImporter::new();
491 match importer.import(yaml_content) {
492 Ok(product) => serde_json::to_string(&product).map_err(serialization_error),
493 Err(err) => Err(import_error_to_js(err)),
494 }
495 }
496
497 #[wasm_bindgen]
507 pub fn export_to_odps(product_json: &str) -> Result<String, JsValue> {
508 let product: crate::models::odps::ODPSDataProduct =
509 serde_json::from_str(product_json).map_err(deserialization_error)?;
510 let exporter = crate::export::ODPSExporter;
511 match exporter.export(&product) {
512 Ok(yaml) => Ok(yaml),
513 Err(err) => Err(export_error_to_js(err)),
514 }
515 }
516
517 #[cfg(feature = "odps-validation")]
527 #[wasm_bindgen]
528 pub fn validate_odps(yaml_content: &str) -> Result<(), JsValue> {
529 #[cfg(feature = "cli")]
530 {
531 use crate::cli::validation::validate_odps_internal;
532 validate_odps_internal(yaml_content).map_err(validation_error)
533 }
534 #[cfg(not(feature = "cli"))]
535 {
536 use jsonschema::Validator;
538 use serde_json::Value;
539
540 let schema_content = include_str!("../schemas/odps-json-schema-latest.json");
541 let schema: Value = serde_json::from_str(schema_content)
542 .map_err(|e| validation_error(format!("Failed to load ODPS schema: {}", e)))?;
543
544 let validator = Validator::new(&schema)
545 .map_err(|e| validation_error(format!("Failed to compile ODPS schema: {}", e)))?;
546
547 let data: Value = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
548
549 if let Err(error) = validator.validate(&data) {
550 return Err(validation_error(format!(
551 "ODPS validation failed: {}",
552 error
553 )));
554 }
555
556 Ok(())
557 }
558 }
559
560 #[cfg(not(feature = "odps-validation"))]
561 #[wasm_bindgen]
562 pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
563 Ok(())
566 }
567
568 #[wasm_bindgen]
578 pub fn create_domain(name: &str) -> Result<String, JsValue> {
579 let domain = crate::models::domain::Domain::new(name.to_string());
580 serde_json::to_string(&domain).map_err(serialization_error)
581 }
582
583 #[wasm_bindgen]
593 pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
594 match crate::models::domain::Domain::from_yaml(yaml_content) {
595 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
596 Err(e) => Err(parse_error(e)),
597 }
598 }
599
600 #[wasm_bindgen]
610 pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
611 let domain: crate::models::domain::Domain =
612 serde_json::from_str(domain_json).map_err(deserialization_error)?;
613 domain.to_yaml().map_err(serialization_error)
614 }
615
616 #[wasm_bindgen]
627 pub fn migrate_dataflow_to_domain(
628 dataflow_yaml: &str,
629 domain_name: Option<String>,
630 ) -> Result<String, JsValue> {
631 match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
632 dataflow_yaml,
633 domain_name.as_deref(),
634 ) {
635 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
636 Err(e) => Err(conversion_error(e)),
637 }
638 }
639
640 #[wasm_bindgen]
650 pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
651 use crate::models::Tag;
652 use std::str::FromStr;
653 match Tag::from_str(tag_str) {
654 Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
655 Err(_) => Err(parse_error("Invalid tag format")),
656 }
657 }
658
659 #[wasm_bindgen]
669 pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
670 use crate::models::Tag;
671 let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
672 Ok(tag.to_string())
673 }
674
675 #[wasm_bindgen]
687 pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
688 match crate::convert::convert_to_odcs(input, format.as_deref()) {
689 Ok(yaml) => Ok(yaml),
690 Err(e) => Err(conversion_error(e)),
691 }
692 }
693
694 #[wasm_bindgen]
705 pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
706 let model = deserialize_workspace(workspace_json)?;
707 let filtered = model.filter_nodes_by_owner(owner);
708 serde_json::to_string(&filtered).map_err(serialization_error)
709 }
710
711 #[wasm_bindgen]
722 pub fn filter_relationships_by_owner(
723 workspace_json: &str,
724 owner: &str,
725 ) -> Result<String, JsValue> {
726 let model = deserialize_workspace(workspace_json)?;
727 let filtered = model.filter_relationships_by_owner(owner);
728 serde_json::to_string(&filtered).map_err(serialization_error)
729 }
730
731 #[wasm_bindgen]
742 pub fn filter_nodes_by_infrastructure_type(
743 workspace_json: &str,
744 infrastructure_type: &str,
745 ) -> Result<String, JsValue> {
746 let model = deserialize_workspace(workspace_json)?;
747 let infra_type: crate::models::enums::InfrastructureType =
748 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
749 .map_err(|e| invalid_input_error("infrastructure type", e))?;
750 let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
751 serde_json::to_string(&filtered).map_err(serialization_error)
752 }
753
754 #[wasm_bindgen]
765 pub fn filter_relationships_by_infrastructure_type(
766 workspace_json: &str,
767 infrastructure_type: &str,
768 ) -> Result<String, JsValue> {
769 let model = deserialize_workspace(workspace_json)?;
770 let infra_type: crate::models::enums::InfrastructureType =
771 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
772 .map_err(|e| invalid_input_error("infrastructure type", e))?;
773 let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
774 serde_json::to_string(&filtered).map_err(serialization_error)
775 }
776
777 #[wasm_bindgen]
788 pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
789 let model = deserialize_workspace(workspace_json)?;
790 let (nodes, relationships) = model.filter_by_tags(tag);
791 let result = serde_json::json!({
792 "nodes": nodes,
793 "relationships": relationships
794 });
795 serde_json::to_string(&result).map_err(serialization_error)
796 }
797
798 #[wasm_bindgen]
814 pub fn add_system_to_domain(
815 workspace_json: &str,
816 domain_id: &str,
817 system_json: &str,
818 ) -> Result<String, JsValue> {
819 let mut model = deserialize_workspace(workspace_json)?;
820 let domain_uuid =
821 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
822 let system: crate::models::domain::System =
823 serde_json::from_str(system_json).map_err(deserialization_error)?;
824 model
825 .add_system_to_domain(domain_uuid, system)
826 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
827 serde_json::to_string(&model).map_err(serialization_error)
828 }
829
830 #[wasm_bindgen]
842 pub fn add_cads_node_to_domain(
843 workspace_json: &str,
844 domain_id: &str,
845 node_json: &str,
846 ) -> Result<String, JsValue> {
847 let mut model = deserialize_workspace(workspace_json)?;
848 let domain_uuid =
849 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
850 let node: crate::models::domain::CADSNode =
851 serde_json::from_str(node_json).map_err(deserialization_error)?;
852 model
853 .add_cads_node_to_domain(domain_uuid, node)
854 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
855 serde_json::to_string(&model).map_err(serialization_error)
856 }
857
858 #[wasm_bindgen]
870 pub fn add_odcs_node_to_domain(
871 workspace_json: &str,
872 domain_id: &str,
873 node_json: &str,
874 ) -> Result<String, JsValue> {
875 let mut model = deserialize_workspace(workspace_json)?;
876 let domain_uuid =
877 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
878 let node: crate::models::domain::ODCSNode =
879 serde_json::from_str(node_json).map_err(deserialization_error)?;
880 model
881 .add_odcs_node_to_domain(domain_uuid, node)
882 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
883 serde_json::to_string(&model).map_err(serialization_error)
884 }
885
886 #[wasm_bindgen]
900 pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
901 match crate::validation::input::validate_table_name(name) {
902 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
903 Err(err) => {
904 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
905 }
906 }
907 }
908
909 #[wasm_bindgen]
919 pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
920 match crate::validation::input::validate_column_name(name) {
921 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
922 Err(err) => {
923 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
924 }
925 }
926 }
927
928 #[wasm_bindgen]
938 pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
939 match crate::validation::input::validate_uuid(id) {
940 Ok(uuid) => {
941 Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
942 }
943 Err(err) => {
944 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
945 }
946 }
947 }
948
949 #[wasm_bindgen]
959 pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
960 match crate::validation::input::validate_data_type(data_type) {
961 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
962 Err(err) => {
963 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
964 }
965 }
966 }
967
968 #[wasm_bindgen]
978 pub fn validate_description(desc: &str) -> Result<String, JsValue> {
979 match crate::validation::input::validate_description(desc) {
980 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
981 Err(err) => {
982 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
983 }
984 }
985 }
986
987 #[wasm_bindgen]
998 pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
999 crate::validation::input::sanitize_sql_identifier(name, dialect)
1000 }
1001
1002 #[wasm_bindgen]
1012 pub fn sanitize_description(desc: &str) -> String {
1013 crate::validation::input::sanitize_description(desc)
1014 }
1015
1016 #[wasm_bindgen]
1027 pub fn detect_naming_conflicts(
1028 existing_tables_json: &str,
1029 new_tables_json: &str,
1030 ) -> Result<String, JsValue> {
1031 let existing_tables: Vec<crate::models::Table> =
1032 serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1033 let new_tables: Vec<crate::models::Table> =
1034 serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1035
1036 let validator = crate::validation::tables::TableValidator::new();
1037 let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1038
1039 serde_json::to_string(&conflicts).map_err(serialization_error)
1040 }
1041
1042 #[wasm_bindgen]
1052 pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1053 let table: crate::models::Table =
1054 serde_json::from_str(table_json).map_err(deserialization_error)?;
1055
1056 let validator = crate::validation::tables::TableValidator::new();
1057 match validator.validate_pattern_exclusivity(&table) {
1058 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1059 Err(violation) => {
1060 Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1061 }
1062 }
1063 }
1064
1065 #[wasm_bindgen]
1077 pub fn check_circular_dependency(
1078 relationships_json: &str,
1079 source_table_id: &str,
1080 target_table_id: &str,
1081 ) -> Result<String, JsValue> {
1082 let relationships: Vec<crate::models::Relationship> =
1083 serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1084
1085 let source_id = uuid::Uuid::parse_str(source_table_id)
1086 .map_err(|e| invalid_input_error("source_table_id", e))?;
1087 let target_id = uuid::Uuid::parse_str(target_table_id)
1088 .map_err(|e| invalid_input_error("target_table_id", e))?;
1089
1090 let validator = crate::validation::relationships::RelationshipValidator::new();
1091 match validator.check_circular_dependency(&relationships, source_id, target_id) {
1092 Ok((has_cycle, cycle_path)) => {
1093 let cycle_path_strs: Vec<String> = cycle_path
1094 .map(|path| path.iter().map(|id| id.to_string()).collect())
1095 .unwrap_or_default();
1096 Ok(serde_json::json!({
1097 "has_cycle": has_cycle,
1098 "cycle_path": cycle_path_strs
1099 })
1100 .to_string())
1101 }
1102 Err(err) => Err(validation_error(err)),
1103 }
1104 }
1105
1106 #[wasm_bindgen]
1117 pub fn validate_no_self_reference(
1118 source_table_id: &str,
1119 target_table_id: &str,
1120 ) -> Result<String, JsValue> {
1121 let source_id = uuid::Uuid::parse_str(source_table_id)
1122 .map_err(|e| invalid_input_error("source_table_id", e))?;
1123 let target_id = uuid::Uuid::parse_str(target_table_id)
1124 .map_err(|e| invalid_input_error("target_table_id", e))?;
1125
1126 let validator = crate::validation::relationships::RelationshipValidator::new();
1127 match validator.validate_no_self_reference(source_id, target_id) {
1128 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1129 Err(self_ref) => {
1130 Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1131 }
1132 }
1133 }
1134
1135 #[cfg(feature = "png-export")]
1151 #[wasm_bindgen]
1152 pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1153 let model = deserialize_workspace(workspace_json)?;
1154 let exporter = crate::export::PNGExporter::new();
1155 match exporter.export(&model.tables, width, height) {
1156 Ok(result) => Ok(result.content), Err(err) => Err(export_error_to_js(err)),
1158 }
1159 }
1160
1161 #[wasm_bindgen]
1177 pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1178 let db_name = db_name.to_string();
1179 let store_name = store_name.to_string();
1180 let workspace_path = workspace_path.to_string();
1181
1182 wasm_bindgen_futures::future_to_promise(async move {
1183 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1184 let loader = crate::model::ModelLoader::new(storage);
1185 match loader.load_model(&workspace_path).await {
1186 Ok(result) => serde_json::to_string(&result)
1187 .map(|s| JsValue::from_str(&s))
1188 .map_err(serialization_error),
1189 Err(err) => Err(storage_error(err)),
1190 }
1191 })
1192 }
1193
1194 #[wasm_bindgen]
1207 pub fn save_model(
1208 db_name: &str,
1209 store_name: &str,
1210 workspace_path: &str,
1211 model_json: &str,
1212 ) -> js_sys::Promise {
1213 let db_name = db_name.to_string();
1214 let store_name = store_name.to_string();
1215 let workspace_path = workspace_path.to_string();
1216 let model_json = model_json.to_string();
1217
1218 wasm_bindgen_futures::future_to_promise(async move {
1219 let model: crate::models::DataModel =
1220 serde_json::from_str(&model_json).map_err(deserialization_error)?;
1221
1222 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1223 let saver = crate::model::ModelSaver::new(storage);
1224
1225 for table in &model.tables {
1228 let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1230 let table_data = crate::model::saver::TableData {
1231 id: table.id,
1232 name: table.name.clone(),
1233 yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1234 yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1235 };
1236 saver
1237 .save_table(&workspace_path, &table_data)
1238 .await
1239 .map_err(storage_error)?;
1240 }
1241
1242 if !model.relationships.is_empty() {
1244 let rel_data: Vec<crate::model::saver::RelationshipData> = model
1245 .relationships
1246 .iter()
1247 .map(|rel| {
1248 let yaml_value = serde_json::json!({
1249 "id": rel.id.to_string(),
1250 "source_table_id": rel.source_table_id.to_string(),
1251 "target_table_id": rel.target_table_id.to_string(),
1252 });
1253 let yaml_str = serde_json::to_string(&yaml_value)
1255 .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1256 let yaml_value = serde_yaml::from_str(&yaml_str)
1257 .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1258 Ok(crate::model::saver::RelationshipData {
1259 id: rel.id,
1260 source_table_id: rel.source_table_id,
1261 target_table_id: rel.target_table_id,
1262 yaml_value,
1263 })
1264 })
1265 .collect::<Result<Vec<_>, String>>()
1266 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1267
1268 saver
1269 .save_relationships(&workspace_path, &rel_data)
1270 .await
1271 .map_err(|e| storage_error(e))?;
1272 }
1273
1274 Ok(JsValue::from_str("Model saved successfully"))
1275 })
1276 }
1277
1278 #[cfg(feature = "bpmn")]
1291 #[wasm_bindgen]
1292 pub fn import_bpmn_model(
1293 domain_id: &str,
1294 xml_content: &str,
1295 model_name: Option<String>,
1296 ) -> Result<String, JsValue> {
1297 use crate::import::bpmn::BPMNImporter;
1298 use uuid::Uuid;
1299
1300 let domain_uuid =
1301 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1302
1303 let mut importer = BPMNImporter::new();
1304 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1305 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1306 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1307 }
1308 }
1309
1310 #[cfg(feature = "bpmn")]
1320 #[wasm_bindgen]
1321 pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1322 use crate::export::bpmn::BPMNExporter;
1323 let exporter = BPMNExporter::new();
1324 exporter
1325 .export(xml_content)
1326 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1327 }
1328
1329 #[cfg(feature = "dmn")]
1342 #[wasm_bindgen]
1343 pub fn import_dmn_model(
1344 domain_id: &str,
1345 xml_content: &str,
1346 model_name: Option<String>,
1347 ) -> Result<String, JsValue> {
1348 use crate::import::dmn::DMNImporter;
1349 use uuid::Uuid;
1350
1351 let domain_uuid =
1352 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1353
1354 let mut importer = DMNImporter::new();
1355 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1356 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1357 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1358 }
1359 }
1360
1361 #[cfg(feature = "dmn")]
1371 #[wasm_bindgen]
1372 pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1373 use crate::export::dmn::DMNExporter;
1374 let exporter = DMNExporter::new();
1375 exporter
1376 .export(xml_content)
1377 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1378 }
1379
1380 #[cfg(feature = "openapi")]
1393 #[wasm_bindgen]
1394 pub fn import_openapi_spec(
1395 domain_id: &str,
1396 content: &str,
1397 api_name: Option<String>,
1398 ) -> Result<String, JsValue> {
1399 use crate::import::openapi::OpenAPIImporter;
1400 use uuid::Uuid;
1401
1402 let domain_uuid =
1403 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1404
1405 let mut importer = OpenAPIImporter::new();
1406 match importer.import(content, domain_uuid, api_name.as_deref()) {
1407 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1408 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1409 }
1410 }
1411
1412 #[cfg(feature = "openapi")]
1424 #[wasm_bindgen]
1425 pub fn export_openapi_spec(
1426 content: &str,
1427 source_format: &str,
1428 target_format: Option<String>,
1429 ) -> Result<String, JsValue> {
1430 use crate::export::openapi::OpenAPIExporter;
1431 use crate::models::openapi::OpenAPIFormat;
1432
1433 let source_fmt = match source_format {
1434 "yaml" | "yml" => OpenAPIFormat::Yaml,
1435 "json" => OpenAPIFormat::Json,
1436 _ => {
1437 return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1438 }
1439 };
1440
1441 let target_fmt = if let Some(tf) = target_format {
1442 match tf.as_str() {
1443 "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1444 "json" => Some(OpenAPIFormat::Json),
1445 _ => {
1446 return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1447 }
1448 }
1449 } else {
1450 None
1451 };
1452
1453 let exporter = OpenAPIExporter::new();
1454 exporter
1455 .export(content, source_fmt, target_fmt)
1456 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1457 }
1458
1459 #[cfg(feature = "openapi")]
1471 #[wasm_bindgen]
1472 pub fn convert_openapi_to_odcs(
1473 openapi_content: &str,
1474 component_name: &str,
1475 table_name: Option<String>,
1476 ) -> Result<String, JsValue> {
1477 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1478
1479 let converter = OpenAPIToODCSConverter::new();
1480 match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1481 Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1482 Err(e) => Err(conversion_error(e)),
1483 }
1484 }
1485
1486 #[cfg(feature = "openapi")]
1497 #[wasm_bindgen]
1498 pub fn analyze_openapi_conversion(
1499 openapi_content: &str,
1500 component_name: &str,
1501 ) -> Result<String, JsValue> {
1502 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1503
1504 let converter = OpenAPIToODCSConverter::new();
1505 match converter.analyze_conversion(openapi_content, component_name) {
1506 Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1507 Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1508 .with_code("ANALYSIS_FAILED")
1509 .to_js_value()),
1510 }
1511 }
1512
1513 #[wasm_bindgen]
1528 pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1529 use crate::models::workspace::Workspace;
1530 use chrono::Utc;
1531 use uuid::Uuid;
1532
1533 let owner_uuid =
1534 Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1535
1536 let workspace = Workspace {
1537 id: Uuid::new_v4(),
1538 name: name.to_string(),
1539 owner_id: owner_uuid,
1540 created_at: Utc::now(),
1541 last_modified_at: Utc::now(),
1542 domains: Vec::new(),
1543 };
1544
1545 serde_json::to_string(&workspace).map_err(serialization_error)
1546 }
1547
1548 #[wasm_bindgen]
1558 pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1559 use crate::models::workspace::Workspace;
1560
1561 let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1562 serde_json::to_string(&workspace).map_err(serialization_error)
1563 }
1564
1565 #[wasm_bindgen]
1575 pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1576 use crate::models::workspace::Workspace;
1577
1578 let workspace: Workspace =
1579 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1580 serde_yaml::to_string(&workspace).map_err(serialization_error)
1581 }
1582
1583 #[wasm_bindgen]
1595 pub fn add_domain_to_workspace(
1596 workspace_json: &str,
1597 domain_id: &str,
1598 domain_name: &str,
1599 ) -> Result<String, JsValue> {
1600 use crate::models::workspace::{DomainReference, Workspace};
1601 use chrono::Utc;
1602 use uuid::Uuid;
1603
1604 let mut workspace: Workspace =
1605 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1606 let domain_uuid =
1607 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1608
1609 if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1611 return Err(WasmError::new(
1612 "DuplicateError",
1613 format!("Domain {} already exists in workspace", domain_id),
1614 )
1615 .with_code("DUPLICATE_DOMAIN")
1616 .to_js_value());
1617 }
1618
1619 workspace.domains.push(DomainReference {
1620 id: domain_uuid,
1621 name: domain_name.to_string(),
1622 });
1623 workspace.last_modified_at = Utc::now();
1624
1625 serde_json::to_string(&workspace).map_err(serialization_error)
1626 }
1627
1628 #[wasm_bindgen]
1639 pub fn remove_domain_from_workspace(
1640 workspace_json: &str,
1641 domain_id: &str,
1642 ) -> Result<String, JsValue> {
1643 use crate::models::workspace::Workspace;
1644 use chrono::Utc;
1645 use uuid::Uuid;
1646
1647 let mut workspace: Workspace =
1648 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1649 let domain_uuid =
1650 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1651
1652 let original_len = workspace.domains.len();
1653 workspace.domains.retain(|d| d.id != domain_uuid);
1654
1655 if workspace.domains.len() == original_len {
1656 return Err(WasmError::new(
1657 "NotFoundError",
1658 format!("Domain {} not found in workspace", domain_id),
1659 )
1660 .with_code("DOMAIN_NOT_FOUND")
1661 .to_js_value());
1662 }
1663
1664 workspace.last_modified_at = Utc::now();
1665 serde_json::to_string(&workspace).map_err(serialization_error)
1666 }
1667
1668 #[wasm_bindgen]
1679 pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1680 use crate::models::domain_config::DomainConfig;
1681 use chrono::Utc;
1682 use std::collections::HashMap;
1683 use uuid::Uuid;
1684
1685 let workspace_uuid =
1686 Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1687
1688 let config = DomainConfig {
1689 id: Uuid::new_v4(),
1690 workspace_id: workspace_uuid,
1691 name: name.to_string(),
1692 description: None,
1693 created_at: Utc::now(),
1694 last_modified_at: Utc::now(),
1695 owner: None,
1696 systems: Vec::new(),
1697 tables: Vec::new(),
1698 products: Vec::new(),
1699 assets: Vec::new(),
1700 processes: Vec::new(),
1701 decisions: Vec::new(),
1702 view_positions: HashMap::new(),
1703 folder_path: None,
1704 workspace_path: None,
1705 };
1706
1707 serde_json::to_string(&config).map_err(serialization_error)
1708 }
1709
1710 #[wasm_bindgen]
1720 pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1721 use crate::models::domain_config::DomainConfig;
1722
1723 let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1724 serde_json::to_string(&config).map_err(serialization_error)
1725 }
1726
1727 #[wasm_bindgen]
1737 pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1738 use crate::models::domain_config::DomainConfig;
1739
1740 let config: DomainConfig =
1741 serde_json::from_str(config_json).map_err(deserialization_error)?;
1742 serde_yaml::to_string(&config).map_err(serialization_error)
1743 }
1744
1745 #[wasm_bindgen]
1755 pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1756 use crate::models::domain_config::DomainConfig;
1757
1758 let config: DomainConfig =
1759 serde_json::from_str(config_json).map_err(deserialization_error)?;
1760 Ok(config.id.to_string())
1761 }
1762
1763 #[wasm_bindgen]
1774 pub fn update_domain_view_positions(
1775 config_json: &str,
1776 positions_json: &str,
1777 ) -> Result<String, JsValue> {
1778 use crate::models::domain_config::{DomainConfig, ViewPosition};
1779 use chrono::Utc;
1780 use std::collections::HashMap;
1781
1782 let mut config: DomainConfig =
1783 serde_json::from_str(config_json).map_err(deserialization_error)?;
1784 let positions: HashMap<String, HashMap<String, ViewPosition>> =
1785 serde_json::from_str(positions_json).map_err(deserialization_error)?;
1786
1787 config.view_positions = positions;
1788 config.last_modified_at = Utc::now();
1789
1790 serde_json::to_string(&config).map_err(serialization_error)
1791 }
1792
1793 #[wasm_bindgen]
1805 pub fn add_entity_to_domain_config(
1806 config_json: &str,
1807 entity_type: &str,
1808 entity_id: &str,
1809 ) -> Result<String, JsValue> {
1810 use crate::models::domain_config::DomainConfig;
1811 use chrono::Utc;
1812 use uuid::Uuid;
1813
1814 let mut config: DomainConfig =
1815 serde_json::from_str(config_json).map_err(deserialization_error)?;
1816 let entity_uuid =
1817 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
1818
1819 let entities = match entity_type {
1820 "system" => &mut config.systems,
1821 "table" => &mut config.tables,
1822 "product" => &mut config.products,
1823 "asset" => &mut config.assets,
1824 "process" => &mut config.processes,
1825 "decision" => &mut config.decisions,
1826 _ => {
1827 return Err(invalid_input_error(
1828 "entity type",
1829 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
1830 ));
1831 }
1832 };
1833
1834 if entities.contains(&entity_uuid) {
1835 return Err(WasmError::new(
1836 "DuplicateError",
1837 format!(
1838 "{} {} already exists in domain config",
1839 entity_type, entity_id
1840 ),
1841 )
1842 .with_code("DUPLICATE_ENTITY")
1843 .to_js_value());
1844 }
1845
1846 entities.push(entity_uuid);
1847 config.last_modified_at = Utc::now();
1848
1849 serde_json::to_string(&config).map_err(serialization_error)
1850 }
1851
1852 #[wasm_bindgen]
1864 pub fn remove_entity_from_domain_config(
1865 config_json: &str,
1866 entity_type: &str,
1867 entity_id: &str,
1868 ) -> Result<String, JsValue> {
1869 use crate::models::domain_config::DomainConfig;
1870 use chrono::Utc;
1871 use uuid::Uuid;
1872
1873 let mut config: DomainConfig =
1874 serde_json::from_str(config_json).map_err(deserialization_error)?;
1875 let entity_uuid =
1876 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
1877
1878 let entities = match entity_type {
1879 "system" => &mut config.systems,
1880 "table" => &mut config.tables,
1881 "product" => &mut config.products,
1882 "asset" => &mut config.assets,
1883 "process" => &mut config.processes,
1884 "decision" => &mut config.decisions,
1885 _ => {
1886 return Err(invalid_input_error(
1887 "entity type",
1888 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
1889 ));
1890 }
1891 };
1892
1893 let original_len = entities.len();
1894 entities.retain(|id| *id != entity_uuid);
1895
1896 if entities.len() == original_len {
1897 return Err(WasmError::new(
1898 "NotFoundError",
1899 format!("{} {} not found in domain config", entity_type, entity_id),
1900 )
1901 .with_code("ENTITY_NOT_FOUND")
1902 .to_js_value());
1903 }
1904
1905 config.last_modified_at = Utc::now();
1906 serde_json::to_string(&config).map_err(serialization_error)
1907 }
1908}