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 });
262
263 for nested_col in nested_cols {
265 all_columns.push(ColumnData {
266 name: nested_col.name,
267 data_type: nested_col.data_type,
268 physical_type: nested_col.physical_type,
269 nullable: nested_col.nullable,
270 primary_key: nested_col.primary_key,
271 description: if nested_col.description.is_empty() {
272 None
273 } else {
274 Some(nested_col.description)
275 },
276 quality: if nested_col.quality.is_empty() {
277 None
278 } else {
279 Some(nested_col.quality)
280 },
281 relationships: nested_col.relationships,
282 enum_values: if nested_col.enum_values.is_empty() {
283 None
284 } else {
285 Some(nested_col.enum_values)
286 },
287 });
288 }
289 continue;
290 }
291 }
292 }
293
294 all_columns.push(col_data);
296 }
297
298 TableData {
299 table_index: table_data.table_index,
300 name: table_data.name,
301 columns: all_columns,
302 }
303 })
304 .collect();
305
306 ImportResult {
307 tables,
308 tables_requiring_name: result.tables_requiring_name,
309 errors: result.errors,
310 ai_suggestions: result.ai_suggestions,
311 }
312 }
313
314 fn deserialize_workspace(json: &str) -> Result<DataModel, JsValue> {
316 serde_json::from_str(json).map_err(deserialization_error)
317 }
318
319 #[wasm_bindgen]
329 pub fn parse_odcs_yaml(yaml_content: &str) -> Result<String, JsValue> {
330 let mut importer = crate::import::ODCSImporter::new();
331 match importer.import(yaml_content) {
332 Ok(result) => {
333 let flattened = flatten_struct_columns(result);
334 serialize_import_result(&flattened)
335 }
336 Err(err) => Err(import_error_to_js(err)),
337 }
338 }
339
340 #[wasm_bindgen]
356 pub fn parse_odcl_yaml(yaml_content: &str) -> Result<String, JsValue> {
357 let mut importer = crate::import::ODCLImporter::new();
358 match importer.import(yaml_content) {
359 Ok(result) => {
360 let flattened = flatten_struct_columns(result);
361 serialize_import_result(&flattened)
362 }
363 Err(err) => Err(import_error_to_js(err)),
364 }
365 }
366
367 #[wasm_bindgen]
380 pub fn is_odcl_format(yaml_content: &str) -> bool {
381 let importer = crate::import::ODCLImporter::new();
382 importer.can_handle(yaml_content)
383 }
384
385 #[wasm_bindgen]
395 pub fn export_to_odcs_yaml(workspace_json: &str) -> Result<String, JsValue> {
396 let model = deserialize_workspace(workspace_json)?;
397
398 let exports = crate::export::ODCSExporter::export_model(&model, None, "odcs_v3_1_0");
400
401 let yaml_docs: Vec<String> = exports.values().cloned().collect();
403 Ok(yaml_docs.join("\n---\n"))
404 }
405
406 #[wasm_bindgen]
417 pub fn import_from_sql(sql_content: &str, dialect: &str) -> Result<String, JsValue> {
418 let importer = crate::import::SQLImporter::new(dialect);
419 match importer.parse(sql_content) {
420 Ok(result) => {
421 let flattened = flatten_struct_columns(result);
423 serialize_import_result(&flattened)
424 }
425 Err(err) => Err(parse_error(err)),
426 }
427 }
428
429 #[wasm_bindgen]
439 pub fn import_from_avro(avro_content: &str) -> Result<String, JsValue> {
440 let importer = crate::import::AvroImporter::new();
441 match importer.import(avro_content) {
442 Ok(result) => {
443 let flattened = flatten_struct_columns(result);
444 serialize_import_result(&flattened)
445 }
446 Err(err) => Err(import_error_to_js(err)),
447 }
448 }
449
450 #[wasm_bindgen]
460 pub fn import_from_json_schema(json_schema_content: &str) -> Result<String, JsValue> {
461 let importer = crate::import::JSONSchemaImporter::new();
462 match importer.import(json_schema_content) {
463 Ok(result) => {
464 let flattened = flatten_struct_columns(result);
465 serialize_import_result(&flattened)
466 }
467 Err(err) => Err(import_error_to_js(err)),
468 }
469 }
470
471 #[wasm_bindgen]
481 pub fn import_from_protobuf(protobuf_content: &str) -> Result<String, JsValue> {
482 let importer = crate::import::ProtobufImporter::new();
483 match importer.import(protobuf_content) {
484 Ok(result) => {
485 let flattened = flatten_struct_columns(result);
486 serialize_import_result(&flattened)
487 }
488 Err(err) => Err(import_error_to_js(err)),
489 }
490 }
491
492 #[wasm_bindgen]
503 pub fn export_to_sql(workspace_json: &str, dialect: &str) -> Result<String, JsValue> {
504 let model = deserialize_workspace(workspace_json)?;
505 let exporter = crate::export::SQLExporter;
506 match exporter.export(&model.tables, Some(dialect)) {
507 Ok(result) => Ok(result.content),
508 Err(err) => Err(export_error_to_js(err)),
509 }
510 }
511
512 #[wasm_bindgen]
522 pub fn export_to_avro(workspace_json: &str) -> Result<String, JsValue> {
523 let model = deserialize_workspace(workspace_json)?;
524 let exporter = crate::export::AvroExporter;
525 match exporter.export(&model.tables) {
526 Ok(result) => Ok(result.content),
527 Err(err) => Err(export_error_to_js(err)),
528 }
529 }
530
531 #[wasm_bindgen]
541 pub fn export_to_json_schema(workspace_json: &str) -> Result<String, JsValue> {
542 let model = deserialize_workspace(workspace_json)?;
543 let exporter = crate::export::JSONSchemaExporter;
544 match exporter.export(&model.tables) {
545 Ok(result) => Ok(result.content),
546 Err(err) => Err(export_error_to_js(err)),
547 }
548 }
549
550 #[wasm_bindgen]
560 pub fn export_to_protobuf(workspace_json: &str) -> Result<String, JsValue> {
561 let model = deserialize_workspace(workspace_json)?;
562 let exporter = crate::export::ProtobufExporter;
563 match exporter.export(&model.tables) {
564 Ok(result) => Ok(result.content),
565 Err(err) => Err(export_error_to_js(err)),
566 }
567 }
568
569 #[wasm_bindgen]
579 pub fn import_from_cads(yaml_content: &str) -> Result<String, JsValue> {
580 let importer = crate::import::CADSImporter::new();
581 match importer.import(yaml_content) {
582 Ok(asset) => serde_json::to_string(&asset).map_err(serialization_error),
583 Err(err) => Err(import_error_to_js(err)),
584 }
585 }
586
587 #[wasm_bindgen]
597 pub fn export_to_cads(asset_json: &str) -> Result<String, JsValue> {
598 let asset: crate::models::cads::CADSAsset =
599 serde_json::from_str(asset_json).map_err(deserialization_error)?;
600 let exporter = crate::export::CADSExporter;
601 match exporter.export(&asset) {
602 Ok(yaml) => Ok(yaml),
603 Err(err) => Err(export_error_to_js(err)),
604 }
605 }
606
607 #[wasm_bindgen]
617 pub fn import_from_odps(yaml_content: &str) -> Result<String, JsValue> {
618 let importer = crate::import::ODPSImporter::new();
619 match importer.import(yaml_content) {
620 Ok(product) => serde_json::to_string(&product).map_err(serialization_error),
621 Err(err) => Err(import_error_to_js(err)),
622 }
623 }
624
625 #[wasm_bindgen]
635 pub fn export_to_odps(product_json: &str) -> Result<String, JsValue> {
636 let product: crate::models::odps::ODPSDataProduct =
637 serde_json::from_str(product_json).map_err(deserialization_error)?;
638 let exporter = crate::export::ODPSExporter;
639 match exporter.export(&product) {
640 Ok(yaml) => Ok(yaml),
641 Err(err) => Err(export_error_to_js(err)),
642 }
643 }
644
645 #[cfg(feature = "odps-validation")]
655 #[wasm_bindgen]
656 pub fn validate_odps(yaml_content: &str) -> Result<(), JsValue> {
657 #[cfg(feature = "cli")]
658 {
659 use crate::cli::validation::validate_odps_internal;
660 validate_odps_internal(yaml_content).map_err(validation_error)
661 }
662 #[cfg(not(feature = "cli"))]
663 {
664 use jsonschema::Validator;
666 use serde_json::Value;
667
668 let schema_content = include_str!("../schemas/odps-json-schema-latest.json");
669 let schema: Value = serde_json::from_str(schema_content)
670 .map_err(|e| validation_error(format!("Failed to load ODPS schema: {}", e)))?;
671
672 let validator = Validator::new(&schema)
673 .map_err(|e| validation_error(format!("Failed to compile ODPS schema: {}", e)))?;
674
675 let data: Value = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
676
677 if let Err(error) = validator.validate(&data) {
678 return Err(validation_error(format!(
679 "ODPS validation failed: {}",
680 error
681 )));
682 }
683
684 Ok(())
685 }
686 }
687
688 #[cfg(not(feature = "odps-validation"))]
689 #[wasm_bindgen]
690 pub fn validate_odps(_yaml_content: &str) -> Result<(), JsValue> {
691 Ok(())
694 }
695
696 #[wasm_bindgen]
706 pub fn create_domain(name: &str) -> Result<String, JsValue> {
707 let domain = crate::models::domain::Domain::new(name.to_string());
708 serde_json::to_string(&domain).map_err(serialization_error)
709 }
710
711 #[wasm_bindgen]
721 pub fn import_from_domain(yaml_content: &str) -> Result<String, JsValue> {
722 match crate::models::domain::Domain::from_yaml(yaml_content) {
723 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
724 Err(e) => Err(parse_error(e)),
725 }
726 }
727
728 #[wasm_bindgen]
738 pub fn export_to_domain(domain_json: &str) -> Result<String, JsValue> {
739 let domain: crate::models::domain::Domain =
740 serde_json::from_str(domain_json).map_err(deserialization_error)?;
741 domain.to_yaml().map_err(serialization_error)
742 }
743
744 #[wasm_bindgen]
755 pub fn migrate_dataflow_to_domain(
756 dataflow_yaml: &str,
757 domain_name: Option<String>,
758 ) -> Result<String, JsValue> {
759 match crate::convert::migrate_dataflow::migrate_dataflow_to_domain(
760 dataflow_yaml,
761 domain_name.as_deref(),
762 ) {
763 Ok(domain) => serde_json::to_string(&domain).map_err(serialization_error),
764 Err(e) => Err(conversion_error(e)),
765 }
766 }
767
768 #[wasm_bindgen]
778 pub fn parse_tag(tag_str: &str) -> Result<String, JsValue> {
779 use crate::models::Tag;
780 use std::str::FromStr;
781 match Tag::from_str(tag_str) {
782 Ok(tag) => serde_json::to_string(&tag).map_err(serialization_error),
783 Err(_) => Err(parse_error("Invalid tag format")),
784 }
785 }
786
787 #[wasm_bindgen]
797 pub fn serialize_tag(tag_json: &str) -> Result<String, JsValue> {
798 use crate::models::Tag;
799 let tag: Tag = serde_json::from_str(tag_json).map_err(deserialization_error)?;
800 Ok(tag.to_string())
801 }
802
803 #[wasm_bindgen]
815 pub fn convert_to_odcs(input: &str, format: Option<String>) -> Result<String, JsValue> {
816 match crate::convert::convert_to_odcs(input, format.as_deref()) {
817 Ok(yaml) => Ok(yaml),
818 Err(e) => Err(conversion_error(e)),
819 }
820 }
821
822 #[wasm_bindgen]
833 pub fn filter_nodes_by_owner(workspace_json: &str, owner: &str) -> Result<String, JsValue> {
834 let model = deserialize_workspace(workspace_json)?;
835 let filtered = model.filter_nodes_by_owner(owner);
836 serde_json::to_string(&filtered).map_err(serialization_error)
837 }
838
839 #[wasm_bindgen]
850 pub fn filter_relationships_by_owner(
851 workspace_json: &str,
852 owner: &str,
853 ) -> Result<String, JsValue> {
854 let model = deserialize_workspace(workspace_json)?;
855 let filtered = model.filter_relationships_by_owner(owner);
856 serde_json::to_string(&filtered).map_err(serialization_error)
857 }
858
859 #[wasm_bindgen]
870 pub fn filter_nodes_by_infrastructure_type(
871 workspace_json: &str,
872 infrastructure_type: &str,
873 ) -> Result<String, JsValue> {
874 let model = deserialize_workspace(workspace_json)?;
875 let infra_type: crate::models::enums::InfrastructureType =
876 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
877 .map_err(|e| invalid_input_error("infrastructure type", e))?;
878 let filtered = model.filter_nodes_by_infrastructure_type(infra_type);
879 serde_json::to_string(&filtered).map_err(serialization_error)
880 }
881
882 #[wasm_bindgen]
893 pub fn filter_relationships_by_infrastructure_type(
894 workspace_json: &str,
895 infrastructure_type: &str,
896 ) -> Result<String, JsValue> {
897 let model = deserialize_workspace(workspace_json)?;
898 let infra_type: crate::models::enums::InfrastructureType =
899 serde_json::from_str(&format!("\"{}\"", infrastructure_type))
900 .map_err(|e| invalid_input_error("infrastructure type", e))?;
901 let filtered = model.filter_relationships_by_infrastructure_type(infra_type);
902 serde_json::to_string(&filtered).map_err(serialization_error)
903 }
904
905 #[wasm_bindgen]
916 pub fn filter_by_tags(workspace_json: &str, tag: &str) -> Result<String, JsValue> {
917 let model = deserialize_workspace(workspace_json)?;
918 let (nodes, relationships) = model.filter_by_tags(tag);
919 let result = serde_json::json!({
920 "nodes": nodes,
921 "relationships": relationships
922 });
923 serde_json::to_string(&result).map_err(serialization_error)
924 }
925
926 #[wasm_bindgen]
942 pub fn add_system_to_domain(
943 workspace_json: &str,
944 domain_id: &str,
945 system_json: &str,
946 ) -> Result<String, JsValue> {
947 let mut model = deserialize_workspace(workspace_json)?;
948 let domain_uuid =
949 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
950 let system: crate::models::domain::System =
951 serde_json::from_str(system_json).map_err(deserialization_error)?;
952 model
953 .add_system_to_domain(domain_uuid, system)
954 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
955 serde_json::to_string(&model).map_err(serialization_error)
956 }
957
958 #[wasm_bindgen]
970 pub fn add_cads_node_to_domain(
971 workspace_json: &str,
972 domain_id: &str,
973 node_json: &str,
974 ) -> Result<String, JsValue> {
975 let mut model = deserialize_workspace(workspace_json)?;
976 let domain_uuid =
977 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
978 let node: crate::models::domain::CADSNode =
979 serde_json::from_str(node_json).map_err(deserialization_error)?;
980 model
981 .add_cads_node_to_domain(domain_uuid, node)
982 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
983 serde_json::to_string(&model).map_err(serialization_error)
984 }
985
986 #[wasm_bindgen]
998 pub fn add_odcs_node_to_domain(
999 workspace_json: &str,
1000 domain_id: &str,
1001 node_json: &str,
1002 ) -> Result<String, JsValue> {
1003 let mut model = deserialize_workspace(workspace_json)?;
1004 let domain_uuid =
1005 uuid::Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1006 let node: crate::models::domain::ODCSNode =
1007 serde_json::from_str(node_json).map_err(deserialization_error)?;
1008 model
1009 .add_odcs_node_to_domain(domain_uuid, node)
1010 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1011 serde_json::to_string(&model).map_err(serialization_error)
1012 }
1013
1014 #[wasm_bindgen]
1028 pub fn validate_table_name(name: &str) -> Result<String, JsValue> {
1029 match crate::validation::input::validate_table_name(name) {
1030 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1031 Err(err) => {
1032 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1033 }
1034 }
1035 }
1036
1037 #[wasm_bindgen]
1047 pub fn validate_column_name(name: &str) -> Result<String, JsValue> {
1048 match crate::validation::input::validate_column_name(name) {
1049 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1050 Err(err) => {
1051 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1052 }
1053 }
1054 }
1055
1056 #[wasm_bindgen]
1066 pub fn validate_uuid(id: &str) -> Result<String, JsValue> {
1067 match crate::validation::input::validate_uuid(id) {
1068 Ok(uuid) => {
1069 Ok(serde_json::json!({"valid": true, "uuid": uuid.to_string()}).to_string())
1070 }
1071 Err(err) => {
1072 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1073 }
1074 }
1075 }
1076
1077 #[wasm_bindgen]
1087 pub fn validate_data_type(data_type: &str) -> Result<String, JsValue> {
1088 match crate::validation::input::validate_data_type(data_type) {
1089 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1090 Err(err) => {
1091 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1092 }
1093 }
1094 }
1095
1096 #[wasm_bindgen]
1106 pub fn validate_description(desc: &str) -> Result<String, JsValue> {
1107 match crate::validation::input::validate_description(desc) {
1108 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1109 Err(err) => {
1110 Ok(serde_json::json!({"valid": false, "error": err.to_string()}).to_string())
1111 }
1112 }
1113 }
1114
1115 #[wasm_bindgen]
1126 pub fn sanitize_sql_identifier(name: &str, dialect: &str) -> String {
1127 crate::validation::input::sanitize_sql_identifier(name, dialect)
1128 }
1129
1130 #[wasm_bindgen]
1140 pub fn sanitize_description(desc: &str) -> String {
1141 crate::validation::input::sanitize_description(desc)
1142 }
1143
1144 #[wasm_bindgen]
1155 pub fn detect_naming_conflicts(
1156 existing_tables_json: &str,
1157 new_tables_json: &str,
1158 ) -> Result<String, JsValue> {
1159 let existing_tables: Vec<crate::models::Table> =
1160 serde_json::from_str(existing_tables_json).map_err(deserialization_error)?;
1161 let new_tables: Vec<crate::models::Table> =
1162 serde_json::from_str(new_tables_json).map_err(deserialization_error)?;
1163
1164 let validator = crate::validation::tables::TableValidator::new();
1165 let conflicts = validator.detect_naming_conflicts(&existing_tables, &new_tables);
1166
1167 serde_json::to_string(&conflicts).map_err(serialization_error)
1168 }
1169
1170 #[wasm_bindgen]
1180 pub fn validate_pattern_exclusivity(table_json: &str) -> Result<String, JsValue> {
1181 let table: crate::models::Table =
1182 serde_json::from_str(table_json).map_err(deserialization_error)?;
1183
1184 let validator = crate::validation::tables::TableValidator::new();
1185 match validator.validate_pattern_exclusivity(&table) {
1186 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1187 Err(violation) => {
1188 Ok(serde_json::json!({"valid": false, "violation": violation}).to_string())
1189 }
1190 }
1191 }
1192
1193 #[wasm_bindgen]
1205 pub fn check_circular_dependency(
1206 relationships_json: &str,
1207 source_table_id: &str,
1208 target_table_id: &str,
1209 ) -> Result<String, JsValue> {
1210 let relationships: Vec<crate::models::Relationship> =
1211 serde_json::from_str(relationships_json).map_err(deserialization_error)?;
1212
1213 let source_id = uuid::Uuid::parse_str(source_table_id)
1214 .map_err(|e| invalid_input_error("source_table_id", e))?;
1215 let target_id = uuid::Uuid::parse_str(target_table_id)
1216 .map_err(|e| invalid_input_error("target_table_id", e))?;
1217
1218 let validator = crate::validation::relationships::RelationshipValidator::new();
1219 match validator.check_circular_dependency(&relationships, source_id, target_id) {
1220 Ok((has_cycle, cycle_path)) => {
1221 let cycle_path_strs: Vec<String> = cycle_path
1222 .map(|path| path.iter().map(|id| id.to_string()).collect())
1223 .unwrap_or_default();
1224 Ok(serde_json::json!({
1225 "has_cycle": has_cycle,
1226 "cycle_path": cycle_path_strs
1227 })
1228 .to_string())
1229 }
1230 Err(err) => Err(validation_error(err)),
1231 }
1232 }
1233
1234 #[wasm_bindgen]
1245 pub fn validate_no_self_reference(
1246 source_table_id: &str,
1247 target_table_id: &str,
1248 ) -> Result<String, JsValue> {
1249 let source_id = uuid::Uuid::parse_str(source_table_id)
1250 .map_err(|e| invalid_input_error("source_table_id", e))?;
1251 let target_id = uuid::Uuid::parse_str(target_table_id)
1252 .map_err(|e| invalid_input_error("target_table_id", e))?;
1253
1254 let validator = crate::validation::relationships::RelationshipValidator::new();
1255 match validator.validate_no_self_reference(source_id, target_id) {
1256 Ok(()) => Ok(serde_json::json!({"valid": true}).to_string()),
1257 Err(self_ref) => {
1258 Ok(serde_json::json!({"valid": false, "self_reference": self_ref}).to_string())
1259 }
1260 }
1261 }
1262
1263 #[cfg(feature = "png-export")]
1279 #[wasm_bindgen]
1280 pub fn export_to_png(workspace_json: &str, width: u32, height: u32) -> Result<String, JsValue> {
1281 let model = deserialize_workspace(workspace_json)?;
1282 let exporter = crate::export::PNGExporter::new();
1283 match exporter.export(&model.tables, width, height) {
1284 Ok(result) => Ok(result.content), Err(err) => Err(export_error_to_js(err)),
1286 }
1287 }
1288
1289 #[wasm_bindgen]
1305 pub fn load_model(db_name: &str, store_name: &str, workspace_path: &str) -> js_sys::Promise {
1306 let db_name = db_name.to_string();
1307 let store_name = store_name.to_string();
1308 let workspace_path = workspace_path.to_string();
1309
1310 wasm_bindgen_futures::future_to_promise(async move {
1311 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1312 let loader = crate::model::ModelLoader::new(storage);
1313 match loader.load_model(&workspace_path).await {
1314 Ok(result) => serde_json::to_string(&result)
1315 .map(|s| JsValue::from_str(&s))
1316 .map_err(serialization_error),
1317 Err(err) => Err(storage_error(err)),
1318 }
1319 })
1320 }
1321
1322 #[wasm_bindgen]
1335 pub fn save_model(
1336 db_name: &str,
1337 store_name: &str,
1338 workspace_path: &str,
1339 model_json: &str,
1340 ) -> js_sys::Promise {
1341 let db_name = db_name.to_string();
1342 let store_name = store_name.to_string();
1343 let workspace_path = workspace_path.to_string();
1344 let model_json = model_json.to_string();
1345
1346 wasm_bindgen_futures::future_to_promise(async move {
1347 let model: crate::models::DataModel =
1348 serde_json::from_str(&model_json).map_err(deserialization_error)?;
1349
1350 let storage = crate::storage::browser::BrowserStorageBackend::new(db_name, store_name);
1351 let saver = crate::model::ModelSaver::new(storage);
1352
1353 for table in &model.tables {
1356 let yaml = crate::export::ODCSExporter::export_table(table, "odcs_v3_1_0");
1358 let table_data = crate::model::saver::TableData {
1359 id: table.id,
1360 name: table.name.clone(),
1361 yaml_file_path: Some(format!("tables/{}.yaml", table.name)),
1362 yaml_value: serde_yaml::from_str(&yaml).map_err(parse_error)?,
1363 };
1364 saver
1365 .save_table(&workspace_path, &table_data)
1366 .await
1367 .map_err(storage_error)?;
1368 }
1369
1370 if !model.relationships.is_empty() {
1372 let rel_data: Vec<crate::model::saver::RelationshipData> = model
1373 .relationships
1374 .iter()
1375 .map(|rel| {
1376 let yaml_value = serde_json::json!({
1377 "id": rel.id.to_string(),
1378 "source_table_id": rel.source_table_id.to_string(),
1379 "target_table_id": rel.target_table_id.to_string(),
1380 });
1381 let yaml_str = serde_json::to_string(&yaml_value)
1383 .map_err(|e| format!("Failed to serialize relationship: {}", e))?;
1384 let yaml_value = serde_yaml::from_str(&yaml_str)
1385 .map_err(|e| format!("Failed to convert to YAML: {}", e))?;
1386 Ok(crate::model::saver::RelationshipData {
1387 id: rel.id,
1388 source_table_id: rel.source_table_id,
1389 target_table_id: rel.target_table_id,
1390 yaml_value,
1391 })
1392 })
1393 .collect::<Result<Vec<_>, String>>()
1394 .map_err(|e| WasmError::new("OperationError", e).to_js_value())?;
1395
1396 saver
1397 .save_relationships(&workspace_path, &rel_data)
1398 .await
1399 .map_err(|e| storage_error(e))?;
1400 }
1401
1402 Ok(JsValue::from_str("Model saved successfully"))
1403 })
1404 }
1405
1406 #[cfg(feature = "bpmn")]
1419 #[wasm_bindgen]
1420 pub fn import_bpmn_model(
1421 domain_id: &str,
1422 xml_content: &str,
1423 model_name: Option<String>,
1424 ) -> Result<String, JsValue> {
1425 use crate::import::bpmn::BPMNImporter;
1426 use uuid::Uuid;
1427
1428 let domain_uuid =
1429 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1430
1431 let mut importer = BPMNImporter::new();
1432 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1433 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1434 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1435 }
1436 }
1437
1438 #[cfg(feature = "bpmn")]
1448 #[wasm_bindgen]
1449 pub fn export_bpmn_model(xml_content: &str) -> Result<String, JsValue> {
1450 use crate::export::bpmn::BPMNExporter;
1451 let exporter = BPMNExporter::new();
1452 exporter
1453 .export(xml_content)
1454 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1455 }
1456
1457 #[cfg(feature = "dmn")]
1470 #[wasm_bindgen]
1471 pub fn import_dmn_model(
1472 domain_id: &str,
1473 xml_content: &str,
1474 model_name: Option<String>,
1475 ) -> Result<String, JsValue> {
1476 use crate::import::dmn::DMNImporter;
1477 use uuid::Uuid;
1478
1479 let domain_uuid =
1480 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1481
1482 let mut importer = DMNImporter::new();
1483 match importer.import(xml_content, domain_uuid, model_name.as_deref()) {
1484 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1485 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1486 }
1487 }
1488
1489 #[cfg(feature = "dmn")]
1499 #[wasm_bindgen]
1500 pub fn export_dmn_model(xml_content: &str) -> Result<String, JsValue> {
1501 use crate::export::dmn::DMNExporter;
1502 let exporter = DMNExporter::new();
1503 exporter
1504 .export(xml_content)
1505 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1506 }
1507
1508 #[cfg(feature = "openapi")]
1521 #[wasm_bindgen]
1522 pub fn import_openapi_spec(
1523 domain_id: &str,
1524 content: &str,
1525 api_name: Option<String>,
1526 ) -> Result<String, JsValue> {
1527 use crate::import::openapi::OpenAPIImporter;
1528 use uuid::Uuid;
1529
1530 let domain_uuid =
1531 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1532
1533 let mut importer = OpenAPIImporter::new();
1534 match importer.import(content, domain_uuid, api_name.as_deref()) {
1535 Ok(model) => serde_json::to_string(&model).map_err(serialization_error),
1536 Err(e) => Err(import_error_to_js(ImportError::ParseError(e.to_string()))),
1537 }
1538 }
1539
1540 #[cfg(feature = "openapi")]
1552 #[wasm_bindgen]
1553 pub fn export_openapi_spec(
1554 content: &str,
1555 source_format: &str,
1556 target_format: Option<String>,
1557 ) -> Result<String, JsValue> {
1558 use crate::export::openapi::OpenAPIExporter;
1559 use crate::models::openapi::OpenAPIFormat;
1560
1561 let source_fmt = match source_format {
1562 "yaml" | "yml" => OpenAPIFormat::Yaml,
1563 "json" => OpenAPIFormat::Json,
1564 _ => {
1565 return Err(invalid_input_error("source format", "Use 'yaml' or 'json'"));
1566 }
1567 };
1568
1569 let target_fmt = if let Some(tf) = target_format {
1570 match tf.as_str() {
1571 "yaml" | "yml" => Some(OpenAPIFormat::Yaml),
1572 "json" => Some(OpenAPIFormat::Json),
1573 _ => {
1574 return Err(invalid_input_error("target format", "Use 'yaml' or 'json'"));
1575 }
1576 }
1577 } else {
1578 None
1579 };
1580
1581 let exporter = OpenAPIExporter::new();
1582 exporter
1583 .export(content, source_fmt, target_fmt)
1584 .map_err(|e| export_error_to_js(ExportError::SerializationError(e.to_string())))
1585 }
1586
1587 #[cfg(feature = "openapi")]
1599 #[wasm_bindgen]
1600 pub fn convert_openapi_to_odcs(
1601 openapi_content: &str,
1602 component_name: &str,
1603 table_name: Option<String>,
1604 ) -> Result<String, JsValue> {
1605 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1606
1607 let converter = OpenAPIToODCSConverter::new();
1608 match converter.convert_component(openapi_content, component_name, table_name.as_deref()) {
1609 Ok(table) => serde_json::to_string(&table).map_err(serialization_error),
1610 Err(e) => Err(conversion_error(e)),
1611 }
1612 }
1613
1614 #[cfg(feature = "openapi")]
1625 #[wasm_bindgen]
1626 pub fn analyze_openapi_conversion(
1627 openapi_content: &str,
1628 component_name: &str,
1629 ) -> Result<String, JsValue> {
1630 use crate::convert::openapi_to_odcs::OpenAPIToODCSConverter;
1631
1632 let converter = OpenAPIToODCSConverter::new();
1633 match converter.analyze_conversion(openapi_content, component_name) {
1634 Ok(report) => serde_json::to_string(&report).map_err(serialization_error),
1635 Err(e) => Err(WasmError::new("AnalysisError", e.to_string())
1636 .with_code("ANALYSIS_FAILED")
1637 .to_js_value()),
1638 }
1639 }
1640
1641 #[wasm_bindgen]
1656 pub fn create_workspace(name: &str, owner_id: &str) -> Result<String, JsValue> {
1657 use crate::models::workspace::Workspace;
1658 use chrono::Utc;
1659 use uuid::Uuid;
1660
1661 let owner_uuid =
1662 Uuid::parse_str(owner_id).map_err(|e| invalid_input_error("owner ID", e))?;
1663
1664 let workspace = Workspace {
1665 id: Uuid::new_v4(),
1666 name: name.to_string(),
1667 owner_id: owner_uuid,
1668 created_at: Utc::now(),
1669 last_modified_at: Utc::now(),
1670 domains: Vec::new(),
1671 };
1672
1673 serde_json::to_string(&workspace).map_err(serialization_error)
1674 }
1675
1676 #[wasm_bindgen]
1686 pub fn parse_workspace_yaml(yaml_content: &str) -> Result<String, JsValue> {
1687 use crate::models::workspace::Workspace;
1688
1689 let workspace: Workspace = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1690 serde_json::to_string(&workspace).map_err(serialization_error)
1691 }
1692
1693 #[wasm_bindgen]
1703 pub fn export_workspace_to_yaml(workspace_json: &str) -> Result<String, JsValue> {
1704 use crate::models::workspace::Workspace;
1705
1706 let workspace: Workspace =
1707 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1708 serde_yaml::to_string(&workspace).map_err(serialization_error)
1709 }
1710
1711 #[wasm_bindgen]
1723 pub fn add_domain_to_workspace(
1724 workspace_json: &str,
1725 domain_id: &str,
1726 domain_name: &str,
1727 ) -> Result<String, JsValue> {
1728 use crate::models::workspace::{DomainReference, Workspace};
1729 use chrono::Utc;
1730 use uuid::Uuid;
1731
1732 let mut workspace: Workspace =
1733 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1734 let domain_uuid =
1735 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1736
1737 if workspace.domains.iter().any(|d| d.id == domain_uuid) {
1739 return Err(WasmError::new(
1740 "DuplicateError",
1741 format!("Domain {} already exists in workspace", domain_id),
1742 )
1743 .with_code("DUPLICATE_DOMAIN")
1744 .to_js_value());
1745 }
1746
1747 workspace.domains.push(DomainReference {
1748 id: domain_uuid,
1749 name: domain_name.to_string(),
1750 });
1751 workspace.last_modified_at = Utc::now();
1752
1753 serde_json::to_string(&workspace).map_err(serialization_error)
1754 }
1755
1756 #[wasm_bindgen]
1767 pub fn remove_domain_from_workspace(
1768 workspace_json: &str,
1769 domain_id: &str,
1770 ) -> Result<String, JsValue> {
1771 use crate::models::workspace::Workspace;
1772 use chrono::Utc;
1773 use uuid::Uuid;
1774
1775 let mut workspace: Workspace =
1776 serde_json::from_str(workspace_json).map_err(deserialization_error)?;
1777 let domain_uuid =
1778 Uuid::parse_str(domain_id).map_err(|e| invalid_input_error("domain ID", e))?;
1779
1780 let original_len = workspace.domains.len();
1781 workspace.domains.retain(|d| d.id != domain_uuid);
1782
1783 if workspace.domains.len() == original_len {
1784 return Err(WasmError::new(
1785 "NotFoundError",
1786 format!("Domain {} not found in workspace", domain_id),
1787 )
1788 .with_code("DOMAIN_NOT_FOUND")
1789 .to_js_value());
1790 }
1791
1792 workspace.last_modified_at = Utc::now();
1793 serde_json::to_string(&workspace).map_err(serialization_error)
1794 }
1795
1796 #[wasm_bindgen]
1807 pub fn create_domain_config(name: &str, workspace_id: &str) -> Result<String, JsValue> {
1808 use crate::models::domain_config::DomainConfig;
1809 use chrono::Utc;
1810 use std::collections::HashMap;
1811 use uuid::Uuid;
1812
1813 let workspace_uuid =
1814 Uuid::parse_str(workspace_id).map_err(|e| invalid_input_error("workspace ID", e))?;
1815
1816 let config = DomainConfig {
1817 id: Uuid::new_v4(),
1818 workspace_id: workspace_uuid,
1819 name: name.to_string(),
1820 description: None,
1821 created_at: Utc::now(),
1822 last_modified_at: Utc::now(),
1823 owner: None,
1824 systems: Vec::new(),
1825 tables: Vec::new(),
1826 products: Vec::new(),
1827 assets: Vec::new(),
1828 processes: Vec::new(),
1829 decisions: Vec::new(),
1830 view_positions: HashMap::new(),
1831 folder_path: None,
1832 workspace_path: None,
1833 };
1834
1835 serde_json::to_string(&config).map_err(serialization_error)
1836 }
1837
1838 #[wasm_bindgen]
1848 pub fn parse_domain_config_yaml(yaml_content: &str) -> Result<String, JsValue> {
1849 use crate::models::domain_config::DomainConfig;
1850
1851 let config: DomainConfig = serde_yaml::from_str(yaml_content).map_err(parse_error)?;
1852 serde_json::to_string(&config).map_err(serialization_error)
1853 }
1854
1855 #[wasm_bindgen]
1865 pub fn export_domain_config_to_yaml(config_json: &str) -> Result<String, JsValue> {
1866 use crate::models::domain_config::DomainConfig;
1867
1868 let config: DomainConfig =
1869 serde_json::from_str(config_json).map_err(deserialization_error)?;
1870 serde_yaml::to_string(&config).map_err(serialization_error)
1871 }
1872
1873 #[wasm_bindgen]
1883 pub fn get_domain_config_id(config_json: &str) -> Result<String, JsValue> {
1884 use crate::models::domain_config::DomainConfig;
1885
1886 let config: DomainConfig =
1887 serde_json::from_str(config_json).map_err(deserialization_error)?;
1888 Ok(config.id.to_string())
1889 }
1890
1891 #[wasm_bindgen]
1902 pub fn update_domain_view_positions(
1903 config_json: &str,
1904 positions_json: &str,
1905 ) -> Result<String, JsValue> {
1906 use crate::models::domain_config::{DomainConfig, ViewPosition};
1907 use chrono::Utc;
1908 use std::collections::HashMap;
1909
1910 let mut config: DomainConfig =
1911 serde_json::from_str(config_json).map_err(deserialization_error)?;
1912 let positions: HashMap<String, HashMap<String, ViewPosition>> =
1913 serde_json::from_str(positions_json).map_err(deserialization_error)?;
1914
1915 config.view_positions = positions;
1916 config.last_modified_at = Utc::now();
1917
1918 serde_json::to_string(&config).map_err(serialization_error)
1919 }
1920
1921 #[wasm_bindgen]
1933 pub fn add_entity_to_domain_config(
1934 config_json: &str,
1935 entity_type: &str,
1936 entity_id: &str,
1937 ) -> Result<String, JsValue> {
1938 use crate::models::domain_config::DomainConfig;
1939 use chrono::Utc;
1940 use uuid::Uuid;
1941
1942 let mut config: DomainConfig =
1943 serde_json::from_str(config_json).map_err(deserialization_error)?;
1944 let entity_uuid =
1945 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
1946
1947 let entities = match entity_type {
1948 "system" => &mut config.systems,
1949 "table" => &mut config.tables,
1950 "product" => &mut config.products,
1951 "asset" => &mut config.assets,
1952 "process" => &mut config.processes,
1953 "decision" => &mut config.decisions,
1954 _ => {
1955 return Err(invalid_input_error(
1956 "entity type",
1957 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
1958 ));
1959 }
1960 };
1961
1962 if entities.contains(&entity_uuid) {
1963 return Err(WasmError::new(
1964 "DuplicateError",
1965 format!(
1966 "{} {} already exists in domain config",
1967 entity_type, entity_id
1968 ),
1969 )
1970 .with_code("DUPLICATE_ENTITY")
1971 .to_js_value());
1972 }
1973
1974 entities.push(entity_uuid);
1975 config.last_modified_at = Utc::now();
1976
1977 serde_json::to_string(&config).map_err(serialization_error)
1978 }
1979
1980 #[wasm_bindgen]
1992 pub fn remove_entity_from_domain_config(
1993 config_json: &str,
1994 entity_type: &str,
1995 entity_id: &str,
1996 ) -> Result<String, JsValue> {
1997 use crate::models::domain_config::DomainConfig;
1998 use chrono::Utc;
1999 use uuid::Uuid;
2000
2001 let mut config: DomainConfig =
2002 serde_json::from_str(config_json).map_err(deserialization_error)?;
2003 let entity_uuid =
2004 Uuid::parse_str(entity_id).map_err(|e| invalid_input_error("entity ID", e))?;
2005
2006 let entities = match entity_type {
2007 "system" => &mut config.systems,
2008 "table" => &mut config.tables,
2009 "product" => &mut config.products,
2010 "asset" => &mut config.assets,
2011 "process" => &mut config.processes,
2012 "decision" => &mut config.decisions,
2013 _ => {
2014 return Err(invalid_input_error(
2015 "entity type",
2016 "Use 'system', 'table', 'product', 'asset', 'process', or 'decision'",
2017 ));
2018 }
2019 };
2020
2021 let original_len = entities.len();
2022 entities.retain(|id| *id != entity_uuid);
2023
2024 if entities.len() == original_len {
2025 return Err(WasmError::new(
2026 "NotFoundError",
2027 format!("{} {} not found in domain config", entity_type, entity_id),
2028 )
2029 .with_code("ENTITY_NOT_FOUND")
2030 .to_js_value());
2031 }
2032
2033 config.last_modified_at = Utc::now();
2034 serde_json::to_string(&config).map_err(serialization_error)
2035 }
2036}