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 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 if is_map {
229 all_columns.push(col_data);
230 continue;
231 }
232
233 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 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 ..Default::default()
262 });
263
264 for nested_col in nested_cols {
266 all_columns.push(
267 crate::import::odcs_shared::column_to_column_data(
268 &nested_col,
269 ),
270 );
271 }
272 continue;
273 }
274 }
275 }
276
277 all_columns.push(col_data);
279 }
280
281 TableData {
282 table_index: table_data.table_index,
283 name: table_data.name,
284 columns: all_columns,
285 }
286 })
287 .collect();
288
289 ImportResult {
290 tables,
291 tables_requiring_name: result.tables_requiring_name,
292 errors: result.errors,
293 ai_suggestions: result.ai_suggestions,
294 }
295 }
296
297 fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
299 serde_json::from_str(json).map_err(deserialization_error)
300 }
301
302 #[wasm_bindgen]
312 pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
313 let mut importer = crate::import::ODCSImporter::new();
314 match importer.import(yaml_content) {
315 Ok(result) => {
316 let flattened = flatten_struct_columns(result);
317 serialize_import_result(&flattened)
318 }
319 Err(err) => Err(import_error_to_js(err)),
320 }
321 }
322
323 #[wasm_bindgen]
339 pub fn parse_odcl_yaml(yaml_content: &str) -> Result<String, JsValue> {
340 let mut importer = crate::import::ODCLImporter::new();
341 match importer.import(yaml_content) {
342 Ok(result) => {
343 let flattened = flatten_struct_columns(result);
344 serialize_import_result(&flattened)
345 }
346 Err(err) => Err(import_error_to_js(err)),
347 }
348 }
349
350 #[wasm_bindgen]
363 pub fn is_odcl_format(yaml_content: &str) -> bool {
364 let importer = crate::import::ODCLImporter::new();
365 importer.can_handle(yaml_content)
366 }
367
368 #[wasm_bindgen]
378 pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
379 let model = deserialize_workspace(workspace_json)?;
380
381 let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
383
384 let yaml_docs: Vec<String> = exports.values().cloned().collect();
386 Ok(yaml_docs.join("\n---\n"))
387 }
388
389 #[wasm_bindgen]
400 pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
401 let importer = crate::import::SQLImporter::new(dialect);
402 match importer.parse(sql_content) {
403 Ok(result) => {
404 let flattened = flatten_struct_columns(result);
406 serialize_import_result(&flattened)
407 }
408 Err(err) => Err(parse_error(err)),
409 }
410 }
411
412 #[wasm_bindgen]
422 pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
423 let importer = crate::import::AvroImporter::new();
424 match importer.import(avro_content) {
425 Ok(result) => {
426 let flattened = flatten_struct_columns(result);
427 serialize_import_result(&flattened)
428 }
429 Err(err) => Err(import_error_to_js(err)),
430 }
431 }
432
433 #[wasm_bindgen]
443 pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
444 let importer = crate::import::JSONSchemaImporter::new();
445 match importer.import(json_schema_content) {
446 Ok(result) => {
447 let flattened = flatten_struct_columns(result);
448 serialize_import_result(&flattened)
449 }
450 Err(err) => Err(import_error_to_js(err)),
451 }
452 }
453
454 #[wasm_bindgen]
464 pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
465 let importer = crate::import::ProtobufImporter::new();
466 match importer.import(protobuf_content) {
467 Ok(result) => {
468 let flattened = flatten_struct_columns(result);
469 serialize_import_result(&flattened)
470 }
471 Err(err) => Err(import_error_to_js(err)),
472 }
473 }
474
475 #[wasm_bindgen]
486 pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
487 let model = deserialize_workspace(workspace_json)?;
488 let exporter = crate::export::SQLExporter;
489 match exporter.export(&model.tables, Some(dialect)) {
490 Ok(result) => Ok(result.content),
491 Err(err) => Err(export_error_to_js(err)),
492 }
493 }
494
495 #[wasm_bindgen]
505 pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
506 let model = deserialize_workspace(workspace_json)?;
507 let exporter = crate::export::AvroExporter;
508 match exporter.export(&model.tables) {
509 Ok(result) => Ok(result.content),
510 Err(err) => Err(export_error_to_js(err)),
511 }
512 }
513
514 #[wasm_bindgen]
524 pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
525 let model = deserialize_workspace(workspace_json)?;
526 let exporter = crate::export::JSONSchemaExporter;
527 match exporter.export(&model.tables) {
528 Ok(result) => Ok(result.content),
529 Err(err) => Err(export_error_to_js(err)),
530 }
531 }
532
533 #[wasm_bindgen]
543 pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
544 let model = deserialize_workspace(workspace_json)?;
545 let exporter = crate::export::ProtobufExporter;
546 match exporter.export(&model.tables) {
547 Ok(result) => Ok(result.content),
548 Err(err) => Err(export_error_to_js(err)),
549 }
550 }
551
552 #[wasm_bindgen]
562 pub fn import_from_cads(yaml_content: &str) -> Result<String, JsValue> {
563 let importer = crate::import::CADSImporter::new();
564 match importer.import(yaml_content) {
565 Ok(asset) => serde_json::to_string(&asset).map_err(serialization_error),
566 Err(err) => Err(import_error_to_js(err)),
567 }
568 }
569
570 #[wasm_bindgen]
580 pub fn export_to_cads(asset_json: &str) -> Result<String, JsValue> {
581 let asset: crate::models::cads::CADSAsset =
582 serde_json::from_str(asset_json).map_err(deserialization_error)?;
583 let exporter = crate::export::CADSExporter;
584 match exporter.export(&asset) {
585 Ok(yaml) => Ok(yaml),
586 Err(err) => Err(export_error_to_js(err)),
587 }
588 }
589
590 #[wasm_bindgen]
600 pub fn import_from_odps(yaml_content: &str) -> Result<String, JsValue> {
601 let importer = crate::import::ODPSImporter::new();
602 match importer.import(yaml_content) {
603 Ok(product) => serde_json::to_string(&product).map_err(serialization_error),
604 Err(err) => Err(import_error_to_js(err)),
605 }
606 }
607
608 #[wasm_bindgen]
618 pub fn export_to_odps(product_json: &str) -> Result<String, JsValue> {
619 let product: crate::models::odps::ODPSDataProduct =
620 serde_json::from_str(product_json).map_err(deserialization_error)?;
621 let exporter = crate::export::ODPSExporter;
622 match exporter.export(&product) {
623 Ok(yaml) => Ok(yaml),
624 Err(err) => Err(export_error_to_js(err)),
625 }
626 }
627
628 #[cfg(feature = "odps-validation")]
638 #[wasm_bindgen]
639 pub fn validate_odps(yaml_content: &str) -> Result<(), JsValue> {
640 #[cfg(feature = "cli")]
641 {
642 use crate::cli::validation::validate_odps_internal;
643 validate_odps_internal(yaml_content).map_err(validation_error)
644 }
645 #[cfg(not(feature = "cli"))]
646 {
647 use jsonschema::Validator;
649 use serde_json::Value;
650
651 let schema_content = include_str!("../schemas/odps-json-schema-latest.json");
652 let schema: Value = serde_json::from_str(schema_content)
653 .map_err(|e| validation_error(format!("Failed to load ODPS schema: {}", e)))?;
654
655 let validator = Validator::new(&schema)
656 .map_err(|e| validation_error(format!("Failed to compile ODPS schema: {}", e)))?;
657
658 let data: Value = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
659
660 if let Err(error) = validator.validate(&data) {
661 return Err(validation_error(format!(
662 "ODPS validation failed: {}",
663 error
664 )));
665 }
666
667 Ok(())
668 }
669 }
670
671 #[cfg(not(feature = "odps-validation"))]
672 #[wasm_bindgen]
673 pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
674 Ok(())
677 }
678
679 #[wasm_bindgen]
689 pub fn create_domain(name: &str) -> Result<String, JsValue> {
690 let domain = crate::models::domain::Domain::new(name.to_string());
691 serde_json::to_string(&domain).map_err(serialization_error)
692 }
693
694 #[wasm_bindgen]
704 pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
705 match crate::models::domain::Domain::from_yaml(yaml_content) {
706 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
707 Err(e) => Err(parse_error(e)),
708 }
709 }
710
711 #[wasm_bindgen]
721 pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
722 let domain: crate::models::domain::Domain =
723 serde_json::from_str(domain_json).map_err(deserialization_error)?;
724 domain.to_yaml().map_err(serialization_error)
725 }
726
727 #[wasm_bindgen]
738 pub fn migrate_dataflow_to_domain(
739 dataflow_yaml: &str,
740 domain_name: Option<String>,
741 ) -> Result<String, JsValue> {
742 match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
743 dataflow_yaml,
744 domain_name.as_deref(),
745 ) {
746 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
747 Err(e) => Err(conversion_error(e)),
748 }
749 }
750
751 #[wasm_bindgen]
761 pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
762 use crate::models::Tag;
763 use std::str::FromStr;
764 match Tag::from_str(tag_str) {
765 Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
766 Err(_) => Err(parse_error("Invalid tag format")),
767 }
768 }
769
770 #[wasm_bindgen]
780 pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
781 use crate::models::Tag;
782 let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
783 Ok(tag.to_string())
784 }
785
786 #[wasm_bindgen]
798 pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
799 match crate::convert::convert_to_odcs(input, format.as_deref()) {
800 Ok(yaml) => Ok(yaml),
801 Err(e) => Err(conversion_error(e)),
802 }
803 }
804
805 #[wasm_bindgen]
816 pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
817 let model = deserialize_workspace(workspace_json)?;
818 let filtered = model.filter_nodes_by_owner(owner);
819 serde_json::to_string(&filtered).map_err(serialization_error)
820 }
821
822 #[wasm_bindgen]
833 pub fn filter_relationships_by_owner(
834 workspace_json: &str,
835 owner: &str,
836 ) -> Result<String, JsValue> {
837 let model = deserialize_workspace(workspace_json)?;
838 let filtered = model.filter_relationships_by_owner(owner);
839 serde_json::to_string(&filtered).map_err(serialization_error)
840 }
841
842 #[wasm_bindgen]
853 pub fn filter_nodes_by_infrastructure_type(
854 workspace_json: &str,
855 infrastructure_type: &str,
856 ) -> Result<String, JsValue> {
857 let model = deserialize_workspace(workspace_json)?;
858 let infra_type: crate::models::enums::InfrastructureType =
859 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
860 .map_err(|e| invalid_input_error("infrastructure type", e))?;
861 let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
862 serde_json::to_string(&filtered).map_err(serialization_error)
863 }
864
865 #[wasm_bindgen]
876 pub fn filter_relationships_by_infrastructure_type(
877 workspace_json: &str,
878 infrastructure_type: &str,
879 ) -> Result<String, JsValue> {
880 let model = deserialize_workspace(workspace_json)?;
881 let infra_type: crate::models::enums::InfrastructureType =
882 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
883 .map_err(|e| invalid_input_error("infrastructure type", e))?;
884 let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
885 serde_json::to_string(&filtered).map_err(serialization_error)
886 }
887
888 #[wasm_bindgen]
899 pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
900 let model = deserialize_workspace(workspace_json)?;
901 let (nodes, relationships) = model.filter_by_tags(tag);
902 let result = serde_json::json!({
903 "nodes": nodes,
904 "relationships": relationships
905 });
906 serde_json::to_string(&result).map_err(serialization_error)
907 }
908
909 #[wasm_bindgen]
925 pub fn add_system_to_domain(
926 workspace_json: &str,
927 domain_id: &str,
928 system_json: &str,
929 ) -> Result<String, JsValue> {
930 let mut model = deserialize_workspace(workspace_json)?;
931 let domain_uuid =
932 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
933 let system: crate::models::domain::System =
934 serde_json::from_str(system_json).map_err(deserialization_error)?;
935 model
936 .add_system_to_domain(domain_uuid, system)
937 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
938 serde_json::to_string(&model).map_err(serialization_error)
939 }
940
941 #[wasm_bindgen]
953 pub fn add_cads_node_to_domain(
954 workspace_json: &str,
955 domain_id: &str,
956 node_json: &str,
957 ) -> Result<String, JsValue> {
958 let mut model = deserialize_workspace(workspace_json)?;
959 let domain_uuid =
960 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
961 let node: crate::models::domain::CADSNode =
962 serde_json::from_str(node_json).map_err(deserialization_error)?;
963 model
964 .add_cads_node_to_domain(domain_uuid, node)
965 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
966 serde_json::to_string(&model).map_err(serialization_error)
967 }
968
969 #[wasm_bindgen]
981 pub fn add_odcs_node_to_domain(
982 workspace_json: &str,
983 domain_id: &str,
984 node_json: &str,
985 ) -> Result<String, JsValue> {
986 let mut model = deserialize_workspace(workspace_json)?;
987 let domain_uuid =
988 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
989 let node: crate::models::domain::ODCSNode =
990 serde_json::from_str(node_json).map_err(deserialization_error)?;
991 model
992 .add_odcs_node_to_domain(domain_uuid, node)
993 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
994 serde_json::to_string(&model).map_err(serialization_error)
995 }
996
997 #[wasm_bindgen]
1011 pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
1012 match crate::validation::input::validate_table_name(name) {
1013 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1014 Err(err) => {
1015 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1016 }
1017 }
1018 }
1019
1020 #[wasm_bindgen]
1030 pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
1031 match crate::validation::input::validate_column_name(name) {
1032 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1033 Err(err) => {
1034 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1035 }
1036 }
1037 }
1038
1039 #[wasm_bindgen]
1049 pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
1050 match crate::validation::input::validate_uuid(id) {
1051 Ok(uuid) => {
1052 Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
1053 }
1054 Err(err) => {
1055 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1056 }
1057 }
1058 }
1059
1060 #[wasm_bindgen]
1070 pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
1071 match crate::validation::input::validate_data_type(data_type) {
1072 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1073 Err(err) => {
1074 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1075 }
1076 }
1077 }
1078
1079 #[wasm_bindgen]
1089 pub fn validate_description(desc: &str) -> Result<String, JsValue> {
1090 match crate::validation::input::validate_description(desc) {
1091 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1092 Err(err) => {
1093 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1094 }
1095 }
1096 }
1097
1098 #[wasm_bindgen]
1109 pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
1110 crate::validation::input::sanitize_sql_identifier(name, dialect)
1111 }
1112
1113 #[wasm_bindgen]
1123 pub fn sanitize_description(desc: &str) -> String {
1124 crate::validation::input::sanitize_description(desc)
1125 }
1126
1127 #[wasm_bindgen]
1138 pub fn detect_naming_conflicts(
1139 existing_tables_json: &str,
1140 new_tables_json: &str,
1141 ) -> Result<String, JsValue> {
1142 let existing_tables: Vec<crate::models::Table> =
1143 serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1144 let new_tables: Vec<crate::models::Table> =
1145 serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1146
1147 let validator = crate::validation::tables::TableValidator::new();
1148 let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1149
1150 serde_json::to_string(&conflicts).map_err(serialization_error)
1151 }
1152
1153 #[wasm_bindgen]
1163 pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1164 let table: crate::models::Table =
1165 serde_json::from_str(table_json).map_err(deserialization_error)?;
1166
1167 let validator = crate::validation::tables::TableValidator::new();
1168 match validator.validate_pattern_exclusivity(&table) {
1169 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1170 Err(violation) => {
1171 Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1172 }
1173 }
1174 }
1175
1176 #[wasm_bindgen]
1188 pub fn check_circular_dependency(
1189 relationships_json: &str,
1190 source_table_id: &str,
1191 target_table_id: &str,
1192 ) -> Result<String, JsValue> {
1193 let relationships: Vec<crate::models::Relationship> =
1194 serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1195
1196 let source_id = uuid::Uuid::parse_str(source_table_id)
1197 .map_err(|e| invalid_input_error("source_table_id", e))?;
1198 let target_id = uuid::Uuid::parse_str(target_table_id)
1199 .map_err(|e| invalid_input_error("target_table_id", e))?;
1200
1201 let validator = crate::validation::relationships::RelationshipValidator::new();
1202 match validator.check_circular_dependency(&relationships, source_id, target_id) {
1203 Ok((has_cycle, cycle_path)) => {
1204 let cycle_path_strs: Vec<String> = cycle_path
1205 .map(|path| path.iter().map(|id| id.to_string()).collect())
1206 .unwrap_or_default();
1207 Ok(serde_json::json!({
1208 "has_cycle": has_cycle,
1209 "cycle_path": cycle_path_strs
1210 })
1211 .to_string())
1212 }
1213 Err(err) => Err(validation_error(err)),
1214 }
1215 }
1216
1217 #[wasm_bindgen]
1228 pub fn validate_no_self_reference(
1229 source_table_id: &str,
1230 target_table_id: &str,
1231 ) -> Result<String, JsValue> {
1232 let source_id = uuid::Uuid::parse_str(source_table_id)
1233 .map_err(|e| invalid_input_error("source_table_id", e))?;
1234 let target_id = uuid::Uuid::parse_str(target_table_id)
1235 .map_err(|e| invalid_input_error("target_table_id", e))?;
1236
1237 let validator = crate::validation::relationships::RelationshipValidator::new();
1238 match validator.validate_no_self_reference(source_id, target_id) {
1239 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1240 Err(self_ref) => {
1241 Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1242 }
1243 }
1244 }
1245
1246 #[cfg(feature = "png-export")]
1262 #[wasm_bindgen]
1263 pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1264 let model = deserialize_workspace(workspace_json)?;
1265 let exporter = crate::export::PNGExporter::new();
1266 match exporter.export(&model.tables, width, height) {
1267 Ok(result) => Ok(result.content), Err(err) => Err(export_error_to_js(err)),
1269 }
1270 }
1271
1272 #[wasm_bindgen]
1288 pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1289 let db_name = db_name.to_string();
1290 let store_name = store_name.to_string();
1291 let workspace_path = workspace_path.to_string();
1292
1293 wasm_bindgen_futures::future_to_promise(async move {
1294 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1295 let loader = crate::model::ModelLoader::new(storage);
1296 match loader.load_model(&workspace_path).await {
1297 Ok(result) => serde_json::to_string(&result)
1298 .map(|s| JsValue::from_str(&s))
1299 .map_err(serialization_error),
1300 Err(err) => Err(storage_error(err)),
1301 }
1302 })
1303 }
1304
1305 #[wasm_bindgen]
1318 pub fn save_model(
1319 db_name: &str,
1320 store_name: &str,
1321 workspace_path: &str,
1322 model_json: &str,
1323 ) -> js_sys::Promise {
1324 let db_name = db_name.to_string();
1325 let store_name = store_name.to_string();
1326 let workspace_path = workspace_path.to_string();
1327 let model_json = model_json.to_string();
1328
1329 wasm_bindgen_futures::future_to_promise(async move {
1330 let model: crate::models::DataModel =
1331 serde_json::from_str(&model_json).map_err(deserialization_error)?;
1332
1333 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1334 let saver = crate::model::ModelSaver::new(storage);
1335
1336 for table in &model.tables {
1339 let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1341 let table_data = crate::model::saver::TableData {
1342 id: table.id,
1343 name: table.name.clone(),
1344 yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1345 yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1346 };
1347 saver
1348 .save_table(&workspace_path, &table_data)
1349 .await
1350 .map_err(storage_error)?;
1351 }
1352
1353 if !model.relationships.is_empty() {
1355 let rel_data: Vec<crate::model::saver::RelationshipData> = model
1356 .relationships
1357 .iter()
1358 .map(|rel| {
1359 let yaml_value = serde_json::json!({
1360 "id": rel.id.to_string(),
1361 "source_table_id": rel.source_table_id.to_string(),
1362 "target_table_id": rel.target_table_id.to_string(),
1363 });
1364 let yaml_str = serde_json::to_string(&yaml_value)
1366 .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1367 let yaml_value = serde_yaml::from_str(&yaml_str)
1368 .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1369 Ok(crate::model::saver::RelationshipData {
1370 id: rel.id,
1371 source_table_id: rel.source_table_id,
1372 target_table_id: rel.target_table_id,
1373 yaml_value,
1374 })
1375 })
1376 .collect::<Result<Vec<_>, String>>()
1377 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1378
1379 saver
1380 .save_relationships(&workspace_path, &rel_data)
1381 .await
1382 .map_err(|e| storage_error(e))?;
1383 }
1384
1385 Ok(JsValue::from_str("Model saved successfully"))
1386 })
1387 }
1388
1389 #[cfg(feature = "bpmn")]
1402 #[wasm_bindgen]
1403 pub fn import_bpmn_model(
1404 domain_id: &str,
1405 xml_content: &str,
1406 model_name: Option<String>,
1407 ) -> Result<String, JsValue> {
1408 use crate::import::bpmn::BPMNImporter;
1409 use uuid::Uuid;
1410
1411 let domain_uuid =
1412 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1413
1414 let mut importer = BPMNImporter::new();
1415 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1416 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1417 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1418 }
1419 }
1420
1421 #[cfg(feature = "bpmn")]
1431 #[wasm_bindgen]
1432 pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1433 use crate::export::bpmn::BPMNExporter;
1434 let exporter = BPMNExporter::new();
1435 exporter
1436 .export(xml_content)
1437 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1438 }
1439
1440 #[cfg(feature = "dmn")]
1453 #[wasm_bindgen]
1454 pub fn import_dmn_model(
1455 domain_id: &str,
1456 xml_content: &str,
1457 model_name: Option<String>,
1458 ) -> Result<String, JsValue> {
1459 use crate::import::dmn::DMNImporter;
1460 use uuid::Uuid;
1461
1462 let domain_uuid =
1463 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1464
1465 let mut importer = DMNImporter::new();
1466 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1467 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1468 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1469 }
1470 }
1471
1472 #[cfg(feature = "dmn")]
1482 #[wasm_bindgen]
1483 pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1484 use crate::export::dmn::DMNExporter;
1485 let exporter = DMNExporter::new();
1486 exporter
1487 .export(xml_content)
1488 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1489 }
1490
1491 #[cfg(feature = "openapi")]
1504 #[wasm_bindgen]
1505 pub fn import_openapi_spec(
1506 domain_id: &str,
1507 content: &str,
1508 api_name: Option<String>,
1509 ) -> Result<String, JsValue> {
1510 use crate::import::openapi::OpenAPIImporter;
1511 use uuid::Uuid;
1512
1513 let domain_uuid =
1514 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1515
1516 let mut importer = OpenAPIImporter::new();
1517 match importer.import(content, domain_uuid, api_name.as_deref()) {
1518 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1519 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1520 }
1521 }
1522
1523 #[cfg(feature = "openapi")]
1535 #[wasm_bindgen]
1536 pub fn export_openapi_spec(
1537 content: &str,
1538 source_format: &str,
1539 target_format: Option<String>,
1540 ) -> Result<String, JsValue> {
1541 use crate::export::openapi::OpenAPIExporter;
1542 use crate::models::openapi::OpenAPIFormat;
1543
1544 let source_fmt = match source_format {
1545 "yaml" | "yml" => OpenAPIFormat::Yaml,
1546 "json" => OpenAPIFormat::Json,
1547 _ => {
1548 return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1549 }
1550 };
1551
1552 let target_fmt = if let Some(tf) = target_format {
1553 match tf.as_str() {
1554 "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1555 "json" => Some(OpenAPIFormat::Json),
1556 _ => {
1557 return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1558 }
1559 }
1560 } else {
1561 None
1562 };
1563
1564 let exporter = OpenAPIExporter::new();
1565 exporter
1566 .export(content, source_fmt, target_fmt)
1567 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1568 }
1569
1570 #[cfg(feature = "openapi")]
1582 #[wasm_bindgen]
1583 pub fn convert_openapi_to_odcs(
1584 openapi_content: &str,
1585 component_name: &str,
1586 table_name: Option<String>,
1587 ) -> Result<String, JsValue> {
1588 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1589
1590 let converter = OpenAPIToODCSConverter::new();
1591 match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1592 Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1593 Err(e) => Err(conversion_error(e)),
1594 }
1595 }
1596
1597 #[cfg(feature = "openapi")]
1608 #[wasm_bindgen]
1609 pub fn analyze_openapi_conversion(
1610 openapi_content: &str,
1611 component_name: &str,
1612 ) -> Result<String, JsValue> {
1613 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1614
1615 let converter = OpenAPIToODCSConverter::new();
1616 match converter.analyze_conversion(openapi_content, component_name) {
1617 Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1618 Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1619 .with_code("ANALYSIS_FAILED")
1620 .to_js_value()),
1621 }
1622 }
1623
1624 #[wasm_bindgen]
1639 pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1640 use crate::models::workspace::Workspace;
1641 use chrono::Utc;
1642 use uuid::Uuid;
1643
1644 let owner_uuid =
1645 Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1646
1647 let workspace = Workspace {
1648 id: Uuid::new_v4(),
1649 name: name.to_string(),
1650 owner_id: owner_uuid,
1651 created_at: Utc::now(),
1652 last_modified_at: Utc::now(),
1653 domains: Vec::new(),
1654 };
1655
1656 serde_json::to_string(&workspace).map_err(serialization_error)
1657 }
1658
1659 #[wasm_bindgen]
1669 pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1670 use crate::models::workspace::Workspace;
1671
1672 let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1673 serde_json::to_string(&workspace).map_err(serialization_error)
1674 }
1675
1676 #[wasm_bindgen]
1686 pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1687 use crate::models::workspace::Workspace;
1688
1689 let workspace: Workspace =
1690 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1691 serde_yaml::to_string(&workspace).map_err(serialization_error)
1692 }
1693
1694 #[wasm_bindgen]
1706 pub fn add_domain_to_workspace(
1707 workspace_json: &str,
1708 domain_id: &str,
1709 domain_name: &str,
1710 ) -> Result<String, JsValue> {
1711 use crate::models::workspace::{DomainReference, Workspace};
1712 use chrono::Utc;
1713 use uuid::Uuid;
1714
1715 let mut workspace: Workspace =
1716 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1717 let domain_uuid =
1718 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1719
1720 if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1722 return Err(WasmError::new(
1723 "DuplicateError",
1724 format!("Domain {} already exists in workspace", domain_id),
1725 )
1726 .with_code("DUPLICATE_DOMAIN")
1727 .to_js_value());
1728 }
1729
1730 workspace.domains.push(DomainReference {
1731 id: domain_uuid,
1732 name: domain_name.to_string(),
1733 });
1734 workspace.last_modified_at = Utc::now();
1735
1736 serde_json::to_string(&workspace).map_err(serialization_error)
1737 }
1738
1739 #[wasm_bindgen]
1750 pub fn remove_domain_from_workspace(
1751 workspace_json: &str,
1752 domain_id: &str,
1753 ) -> Result<String, JsValue> {
1754 use crate::models::workspace::Workspace;
1755 use chrono::Utc;
1756 use uuid::Uuid;
1757
1758 let mut workspace: Workspace =
1759 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1760 let domain_uuid =
1761 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1762
1763 let original_len = workspace.domains.len();
1764 workspace.domains.retain(|d| d.id != domain_uuid);
1765
1766 if workspace.domains.len() == original_len {
1767 return Err(WasmError::new(
1768 "NotFoundError",
1769 format!("Domain {} not found in workspace", domain_id),
1770 )
1771 .with_code("DOMAIN_NOT_FOUND")
1772 .to_js_value());
1773 }
1774
1775 workspace.last_modified_at = Utc::now();
1776 serde_json::to_string(&workspace).map_err(serialization_error)
1777 }
1778
1779 #[wasm_bindgen]
1790 pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1791 use crate::models::domain_config::DomainConfig;
1792 use chrono::Utc;
1793 use std::collections::HashMap;
1794 use uuid::Uuid;
1795
1796 let workspace_uuid =
1797 Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1798
1799 let config = DomainConfig {
1800 id: Uuid::new_v4(),
1801 workspace_id: workspace_uuid,
1802 name: name.to_string(),
1803 description: None,
1804 created_at: Utc::now(),
1805 last_modified_at: Utc::now(),
1806 owner: None,
1807 systems: Vec::new(),
1808 tables: Vec::new(),
1809 products: Vec::new(),
1810 assets: Vec::new(),
1811 processes: Vec::new(),
1812 decisions: Vec::new(),
1813 view_positions: HashMap::new(),
1814 folder_path: None,
1815 workspace_path: None,
1816 };
1817
1818 serde_json::to_string(&config).map_err(serialization_error)
1819 }
1820
1821 #[wasm_bindgen]
1831 pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1832 use crate::models::domain_config::DomainConfig;
1833
1834 let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1835 serde_json::to_string(&config).map_err(serialization_error)
1836 }
1837
1838 #[wasm_bindgen]
1848 pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1849 use crate::models::domain_config::DomainConfig;
1850
1851 let config: DomainConfig =
1852 serde_json::from_str(config_json).map_err(deserialization_error)?;
1853 serde_yaml::to_string(&config).map_err(serialization_error)
1854 }
1855
1856 #[wasm_bindgen]
1866 pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1867 use crate::models::domain_config::DomainConfig;
1868
1869 let config: DomainConfig =
1870 serde_json::from_str(config_json).map_err(deserialization_error)?;
1871 Ok(config.id.to_string())
1872 }
1873
1874 #[wasm_bindgen]
1885 pub fn update_domain_view_positions(
1886 config_json: &str,
1887 positions_json: &str,
1888 ) -> Result<String, JsValue> {
1889 use crate::models::domain_config::{DomainConfig, ViewPosition};
1890 use chrono::Utc;
1891 use std::collections::HashMap;
1892
1893 let mut config: DomainConfig =
1894 serde_json::from_str(config_json).map_err(deserialization_error)?;
1895 let positions: HashMap<String, HashMap<String, ViewPosition>> =
1896 serde_json::from_str(positions_json).map_err(deserialization_error)?;
1897
1898 config.view_positions = positions;
1899 config.last_modified_at = Utc::now();
1900
1901 serde_json::to_string(&config).map_err(serialization_error)
1902 }
1903
1904 #[wasm_bindgen]
1916 pub fn add_entity_to_domain_config(
1917 config_json: &str,
1918 entity_type: &str,
1919 entity_id: &str,
1920 ) -> Result<String, JsValue> {
1921 use crate::models::domain_config::DomainConfig;
1922 use chrono::Utc;
1923 use uuid::Uuid;
1924
1925 let mut config: DomainConfig =
1926 serde_json::from_str(config_json).map_err(deserialization_error)?;
1927 let entity_uuid =
1928 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
1929
1930 let entities = match entity_type {
1931 "system" => &mut config.systems,
1932 "table" => &mut config.tables,
1933 "product" => &mut config.products,
1934 "asset" => &mut config.assets,
1935 "process" => &mut config.processes,
1936 "decision" => &mut config.decisions,
1937 _ => {
1938 return Err(invalid_input_error(
1939 "entity type",
1940 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
1941 ));
1942 }
1943 };
1944
1945 if entities.contains(&entity_uuid) {
1946 return Err(WasmError::new(
1947 "DuplicateError",
1948 format!(
1949 "{} {} already exists in domain config",
1950 entity_type, entity_id
1951 ),
1952 )
1953 .with_code("DUPLICATE_ENTITY")
1954 .to_js_value());
1955 }
1956
1957 entities.push(entity_uuid);
1958 config.last_modified_at = Utc::now();
1959
1960 serde_json::to_string(&config).map_err(serialization_error)
1961 }
1962
1963 #[wasm_bindgen]
1975 pub fn remove_entity_from_domain_config(
1976 config_json: &str,
1977 entity_type: &str,
1978 entity_id: &str,
1979 ) -> Result<String, JsValue> {
1980 use crate::models::domain_config::DomainConfig;
1981 use chrono::Utc;
1982 use uuid::Uuid;
1983
1984 let mut config: DomainConfig =
1985 serde_json::from_str(config_json).map_err(deserialization_error)?;
1986 let entity_uuid =
1987 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
1988
1989 let entities = match entity_type {
1990 "system" => &mut config.systems,
1991 "table" => &mut config.tables,
1992 "product" => &mut config.products,
1993 "asset" => &mut config.assets,
1994 "process" => &mut config.processes,
1995 "decision" => &mut config.decisions,
1996 _ => {
1997 return Err(invalid_input_error(
1998 "entity type",
1999 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2000 ));
2001 }
2002 };
2003
2004 let original_len = entities.len();
2005 entities.retain(|id| *id != entity_uuid);
2006
2007 if entities.len() == original_len {
2008 return Err(WasmError::new(
2009 "NotFoundError",
2010 format!("{} {} not found in domain config", entity_type, entity_id),
2011 )
2012 .with_code("ENTITY_NOT_FOUND")
2013 .to_js_value());
2014 }
2015
2016 config.last_modified_at = Utc::now();
2017 serde_json::to_string(&config).map_err(serialization_error)
2018 }
2019}